字符串拼接的最优方式

87

我们每天都会遇到许多情况,需要在代码中进行繁琐而频繁的字符串操作。我们都知道字符串操作是昂贵的操作。我想知道哪一种方式是最廉价的。

最常见的操作是连接字符串(这是我们在某种程度上可以控制的)。在C++中连接std::strings的最佳方法是什么?以及加速连接的各种解决方案?

我的意思是,

std::string l_czTempStr;

1).l_czTempStr = "Test data1" + "Test data2" + "Test data3";

2). l_czTempStr =  "Test data1"; 
    l_czTempStr += "Test data2";
    l_czTempStr += "Test data3";

3). using << operator

4). using append()

此外,使用 CString 是否比使用 std::string 有任何优势?


6
为什么你无法测量?无论如何,stringstream 是专门用于此场景的,而 string 则不是。因此,最好从 stringstream 开始尝试。 - Magnus Hoff
3
这段话的意思是:除非 l_czTempStr = std::string("Test data1") + "Test data2" + "Test data3"; 是合法的,否则无法回答问题。除此之外,回答需要比较不同的技术所需的时间。由于涉及到许多变量,因此无法回答这个问题。答案取决于您要处理的字符串数量和长度,以及您编译和为其编译的平台。 - john
7
确实是瓶颈吗?那就进行基准测试。一般来说,最快的方法是在附加任何数据之前为所有数据预先分配足够的空间,并避免使用临时变量(+会创建一个新对象,在C++11中有一些特殊情况)。但是,除非必要,否则不要对此进行优化,否则您的代码将难以阅读。 - Dave
6
你理解错了,std::ostringstream 是用于格式化的,通常只在需要格式化输出时使用。所有的数据都是字符串,因此首选解决方案是使用 std::string 和字符串拼接。 - James Kanze
3
顺便提一下:对于非常长的字符串,使用Rope而不是字符串可能是个不错的选择。 - ComicSansMS
显示剩余6条评论
9个回答

78

这是一个小测试套件:

#include <iostream>
#include <string>
#include <chrono>
#include <sstream>

int main ()
{
    typedef std::chrono::high_resolution_clock clock;
    typedef std::chrono::duration<float, std::milli> mil;
    std::string l_czTempStr;
    std::string s1="Test data1";
    auto t0 = clock::now();
    #if VER==1
    for (int i = 0; i < 100000; ++i)
    {
        l_czTempStr = s1 + "Test data2" + "Test data3";
    }
    #elif VER==2
    for (int i = 0; i < 100000; ++i)
    {
        l_czTempStr =  "Test data1"; 
        l_czTempStr += "Test data2";
        l_czTempStr += "Test data3";
    }
    #elif VER==3
    for (int i = 0; i < 100000; ++i)
    {
        l_czTempStr =  "Test data1"; 
        l_czTempStr.append("Test data2");
        l_czTempStr.append("Test data3");
    }
    #elif VER==4
    for (int i = 0; i < 100000; ++i)
    {
        std::ostringstream oss;
        oss << "Test data1";
        oss << "Test data2";
        oss << "Test data3";
        l_czTempStr = oss.str();
    }
    #endif
    auto t1 = clock::now();
    std::cout << l_czTempStr << '\n';
    std::cout << mil(t1-t0).count() << "ms\n";
}

coliru 上:

使用以下命令进行编译:

clang++ -std=c++11 -O3 -DVER=1 -Wall -pedantic -pthread main.cpp

21.6463毫秒

-DVER=2

6.61773毫秒

-DVER=3

6.7855毫秒

-DVER=4

102.015毫秒

看起来 2), += 是胜者。

(同时使用和不使用 -pthread 看起来会影响计时)


1
不错!你的编号3)和4)与问题相反。鉴于差异不太大,看起来唯一的确定结论是要避免使用流。当然,这不仅取决于编译器(版本),还取决于stdlib实现(我认为其中一个是coliru上的GCC)。 - Benjamin Bannier
37
很不幸,这个测试可能不具有代表性。问题在于版本2和版本3没有在循环内包括“l_czTempStr”声明,因此它们重复使用相同的缓冲区,而版本1每次创建一个新的缓冲区“std :: string {""}”。您的基准测试表明,重用相同的缓冲区而不是分配/释放提供了5倍的加速(加速明显,取决于代码块的长度以及如果您不提前保留所有内容会发生多少重新分配)。我不确定OP是否打算重复使用相同的缓冲区。 - Matthieu M.
2
@MatthieuM:+1 好观点。我已经更新了代码,所以在版本1中预先创建了初始字符串,但是operator+仍然在内部遭受大量的分配/释放。 - Jesse Good
3
你的stringstream基准测试包括流构造的(极其)缓慢计时。 - Rapptz
2
“看起来2)+=是赢家。”我不确定你的结果是否显示出与.append()有任何统计差异。也不应该有:其中一个是基于另一个实现的。我不明白它们为什么会有显著的不同。 - underscore_d
显示剩余8条评论

36

除了其他答案之外...

我曾在一段时间内对这个问题进行了广泛的基准测试,并得出结论,在所有用例中,最有效的解决方案(GCC 4.7&4.8在Linux x86 / x64 / ARM上)是先使用reserve()为结果字符串预留足够的空间来容纳所有连接的字符串,然后只需append()它们(或使用operator +=(),这没有区别)。

不幸的是,我似乎已经删除了那个基准测试,所以你只有我的话(但如果我的话不够的话,你可以很容易地调整Mats Petersson的基准测试来验证这一点)。

简而言之:

const string space = " ";
string result;
result.reserve(5 + space.size() + 5);
result += "hello";
result += space;
result += "world";

根据具体用例(连接的字符串数量、类型和大小),有时这种方法是迄今为止效率最高的,而其他情况下则与其他方法相当,但从不劣化。


问题是,在预先计算所需总大小时,这确实很麻烦,尤其是在混合使用字符串文字和std::string时(上面的示例就已经非常清楚了)。只要修改其中一个文字或添加另一个待连接的字符串,这样的代码可维护性就会变得非常糟糕。

一种方法是使用sizeof来计算文本的大小,但我认为这只会制造更多混乱,可维护性仍然很差:

#define STR_HELLO "hello"
#define STR_WORLD "world"

const string space = " ";
string result;
result.reserve(sizeof(STR_HELLO)-1 + space.size() + sizeof(STR_WORLD)-1);
result += STR_HELLO;
result += space;
result += STR_WORLD;

一种可用的解决方案(C++11,可变参数模板)

最终我选择了一组可变参数模板来高效地处理计算字符串大小(例如字符串字面量的大小在编译时确定),根据需要使用reserve(),然后将所有内容连接起来。

这就是它,希望这对您有用:

namespace detail {

  template<typename>
  struct string_size_impl;

  template<size_t N>
  struct string_size_impl<const char[N]> {
    static constexpr size_t size(const char (&) [N]) { return N - 1; }
  };

  template<size_t N>
  struct string_size_impl<char[N]> {
    static size_t size(char (&s) [N]) { return N ? strlen(s) : 0; }
  };

  template<>
  struct string_size_impl<const char*> {
    static size_t size(const char* s) { return s ? strlen(s) : 0; }
  };

  template<>
  struct string_size_impl<char*> {
    static size_t size(char* s) { return s ? strlen(s) : 0; }
  };

  template<>
  struct string_size_impl<std::string> {
    static size_t size(const std::string& s) { return s.size(); }
  };

  template<typename String> size_t string_size(String&& s) {
    using noref_t = typename std::remove_reference<String>::type;
    using string_t = typename std::conditional<std::is_array<noref_t>::value,
                                              noref_t,
                                              typename std::remove_cv<noref_t>::type
                                              >::type;
    return string_size_impl<string_t>::size(s);
  }

  template<typename...>
  struct concatenate_impl;

  template<typename String>
  struct concatenate_impl<String> {
    static size_t size(String&& s) { return string_size(s); }
    static void concatenate(std::string& result, String&& s) { result += s; }
  };

  template<typename String, typename... Rest>
  struct concatenate_impl<String, Rest...> {
    static size_t size(String&& s, Rest&&... rest) {
      return string_size(s)
           + concatenate_impl<Rest...>::size(std::forward<Rest>(rest)...);
    }
    static void concatenate(std::string& result, String&& s, Rest&&... rest) {
      result += s;
      concatenate_impl<Rest...>::concatenate(result, std::forward<Rest>(rest)...);
    }
  };

} // namespace detail

template<typename... Strings>
std::string concatenate(Strings&&... strings) {
  std::string result;
  result.reserve(detail::concatenate_impl<Strings...>::size(std::forward<Strings>(strings)...));
  detail::concatenate_impl<Strings...>::concatenate(result, std::forward<Strings>(strings)...);
  return result;
}

就公共接口而言,唯一有趣的部分是最后一个template<typename... Strings> std::string concatenate(Strings&&... strings)模板。使用方法很简单:

int main() {
  const string space = " ";
  std::string result = concatenate("hello", space, "world");
  std::cout << result << std::endl;
}

开启优化后,任何优秀的编译器都应该能够将concatenate调用扩展为我手动编写一切的第一个例子中的相同代码。就GCC 4.7和4.8而言,生成的代码几乎完全相同,性能也相同。


我不明白在这里使用通用引用的原因。您能解释一下相对于常规(左值)对const的引用,它们可以提供什么优势吗? - Paul Groke
你已经实现了这个wstring吗?看起来应该很简单。 - KarlM
在我的测试中,使用或不使用"reserve"函数没有区别,有时候该函数会更差。 - Hao
@Hao,也许“hello world”适合使用小字符串优化的std::string,因此reserve操作实际上没有任何作用。 - Emile Cormier

27
最糟糕的情况是使用普通的strcat(或sprintf),因为strcat接收C字符串,必须要"计数"才能找到结尾。对于长字符串来说,这会导致性能问题。C++风格的字符串更好,性能问题可能与内存分配有关,而不是计算长度。但是,字符串增长是几何级别的(每次需要增长时都会加倍),所以并不是那么可怕。
我非常怀疑所有上述方法最终的性能都相同或非常相似。如果有什么不同,我预计stringstream会更慢,因为它需要支持格式化的开销,但我也认为这是微不足道的。
由于这种东西很"有趣",我会用一个基准测试返回结果。
注:这些结果适用于我的机器,运行x86-64 Linux,使用g++ 4.6.3编译。其他操作系统、编译器和C++运行库实现可能会有所不同。如果性能对您的应用程序很重要,请在您关键的系统上使用您使用的编译器进行基准测试。以下是我编写的代码。它可能不是真实场景的完美表示,但我认为它是一个典型场景。
#include <iostream>
#include <iomanip>
#include <string>
#include <sstream>
#include <cstring>

using namespace std;

static __inline__ unsigned long long rdtsc(void)
{
    unsigned hi, lo;
    __asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
    return ( (unsigned long long)lo)|( ((unsigned long long)hi)<<32 );
}

string build_string_1(const string &a, const string &b, const string &c)
{
    string out = a + b + c;
    return out;
}

string build_string_1a(const string &a, const string &b, const string &c)
{
    string out;
    out.resize(a.length()*3);
    out = a + b + c;
    return out;
}

string build_string_2(const string &a, const string &b, const string &c)
{
    string out = a;
    out += b;
    out += c;
    return out;
}

string build_string_3(const string &a, const string &b, const string &c)
{
    string out;
    out = a;
    out.append(b);
    out.append(c);
    return out;
}


string build_string_4(const string &a, const string &b, const string &c)
{
    stringstream ss;

    ss << a << b << c;
    return ss.str();
}


char *build_string_5(const char *a, const char *b, const char *c)
{
    char* out = new char[strlen(a) * 3+1];
    strcpy(out, a);
    strcat(out, b);
    strcat(out, c);
    return out;
}



template<typename T>
size_t len(T s)
{
    return s.length();
}

template<>
size_t len(char *s)
{
    return strlen(s);
}

template<>
size_t len(const char *s)
{
    return strlen(s);
}



void result(const char *name, unsigned long long t, const string& out)
{
    cout << left << setw(22) << name << " time:" << right << setw(10) <<  t;
    cout << "   (per character: " 
         << fixed << right << setw(8) << setprecision(2) << (double)t / len(out) << ")" << endl;
}

template<typename T>
void benchmark(const char name[], T (Func)(const T& a, const T& b, const T& c), const char *strings[])
{
    unsigned long long t;

    const T s1 = strings[0];
    const T s2 = strings[1];
    const T s3 = strings[2];
    t = rdtsc();
    T out = Func(s1, s2, s3);
    t = rdtsc() - t; 

    if (len(out) != len(s1) + len(s2) + len(s3))
    {
        cout << "Error: out is different length from inputs" << endl;
        cout << "Got `" << out << "` from `" << s1 << "` + `" << s2 << "` + `" << s3 << "`";
    }
    result(name, t, out);
}


void benchmark(const char name[], char* (Func)(const char* a, const char* b, const char* c), 
               const char *strings[])
{
    unsigned long long t;

    const char* s1 = strings[0];
    const char* s2 = strings[1];
    const char* s3 = strings[2];
    t = rdtsc();
    char *out = Func(s1, s2, s3);
    t = rdtsc() - t; 

    if (len(out) != len(s1) + len(s2) + len(s3))
    {
        cout << "Error: out is different length from inputs" << endl;
        cout << "Got `" << out << "` from `" << s1 << "` + `" << s2 << "` + `" << s3 << "`";
    }
    result(name, t, out);
    delete [] out;
}


#define BM(func, size) benchmark(#func " " #size, func, strings ## _ ## size)


#define BM_LOT(size) BM(build_string_1, size); \
    BM(build_string_1a, size); \
    BM(build_string_2, size); \
    BM(build_string_3, size); \
    BM(build_string_4, size); \
    BM(build_string_5, size);

int main()
{
    const char *strings_small[]  = { "Abc", "Def", "Ghi" };
    const char *strings_medium[] = { "abcdefghijklmnopqrstuvwxyz", 
                                     "defghijklmnopqrstuvwxyzabc", 
                                     "ghijklmnopqrstuvwxyzabcdef" };
    const char *strings_large[]   = 
        { "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
          "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
          "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
          "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
          "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
          "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
          "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
          "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
          "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"
          "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", 

          "defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc" 
          "defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc" 
          "defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc" 
          "defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc" 
          "defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc"

          "defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc" 
          "defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc" 
          "defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc" 
          "defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc" 
          "defghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabc", 

          "ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef"
          "ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef"
          "ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef"
          "ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef"
          "ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef"
          "ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef"
          "ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef"
          "ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef"
          "ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef"
          "ghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdef"
        };

    for(int i = 0; i < 5; i++)
    {
        BM_LOT(small);
        BM_LOT(medium);
        BM_LOT(large);
        cout << "---------------------------------------------" << endl;
    }
}

以下是一些代表性的结果:
build_string_1 small   time:      4075   (per character:   452.78)
build_string_1a small  time:      5384   (per character:   598.22)
build_string_2 small   time:      2669   (per character:   296.56)
build_string_3 small   time:      2427   (per character:   269.67)
build_string_4 small   time:     19380   (per character:  2153.33)
build_string_5 small   time:      6299   (per character:   699.89)
build_string_1 medium  time:      3983   (per character:    51.06)
build_string_1a medium time:      6970   (per character:    89.36)
build_string_2 medium  time:      4072   (per character:    52.21)
build_string_3 medium  time:      4000   (per character:    51.28)
build_string_4 medium  time:     19614   (per character:   251.46)
build_string_5 medium  time:      6304   (per character:    80.82)
build_string_1 large   time:      8491   (per character:     3.63)
build_string_1a large  time:      9563   (per character:     4.09)
build_string_2 large   time:      6154   (per character:     2.63)
build_string_3 large   time:      5992   (per character:     2.56)
build_string_4 large   time:     32450   (per character:    13.87)
build_string_5 large   time:     15768   (per character:     6.74)

相同的代码,以32位运行:

build_string_1 small   time:      4289   (per character:   476.56)
build_string_1a small  time:      5967   (per character:   663.00)
build_string_2 small   time:      3329   (per character:   369.89)
build_string_3 small   time:      3047   (per character:   338.56)
build_string_4 small   time:     22018   (per character:  2446.44)
build_string_5 small   time:      3026   (per character:   336.22)
build_string_1 medium  time:      4089   (per character:    52.42)
build_string_1a medium time:      8075   (per character:   103.53)
build_string_2 medium  time:      4569   (per character:    58.58)
build_string_3 medium  time:      4326   (per character:    55.46)
build_string_4 medium  time:     22751   (per character:   291.68)
build_string_5 medium  time:      2252   (per character:    28.87)
build_string_1 large   time:      8695   (per character:     3.72)
build_string_1a large  time:     12818   (per character:     5.48)
build_string_2 large   time:      8202   (per character:     3.51)
build_string_3 large   time:      8351   (per character:     3.57)
build_string_4 large   time:     38250   (per character:    16.35)
build_string_5 large   time:      8143   (per character:     3.48)

由此可得出以下结论:
  1. 最佳选项是逐个追加位(out.append()out +=),链式方法也相当不错。

  2. 预分配字符串并没有帮助。

  3. 使用 stringstream 是个相当糟糕的想法(速度慢了2-4倍)。

  4. char * 使用了 new char[]。在调用函数中使用局部变量可以使速度更快,但这种比较略微不公平。

  5. 组合短字符串有相当大的开销 - 只需复制数据最多每字节一个周期 [除非数据不适合缓存]。

编辑2 如评论所述,已添加:
string build_string_1b(const string &a, const string &b, const string &c)
{
    return a + b + c;
}

并且。
string build_string_2a(const string &a, const string &b, const string &c)
{
    string out;
    out.reserve(a.length() * 3);
    out += a;
    out += b;
    out += c;
    return out;
}

这将产生以下结果:

build_string_1 small   time:      3845   (per character:   427.22)
build_string_1b small  time:      3165   (per character:   351.67)
build_string_2 small   time:      3176   (per character:   352.89)
build_string_2a small  time:      1904   (per character:   211.56)

build_string_1 large   time:      9056   (per character:     3.87)
build_string_1b large  time:      6414   (per character:     2.74)
build_string_2 large   time:      6417   (per character:     2.74)
build_string_2a large  time:      4179   (per character:     1.79)

(这个测试是在32位系统上进行的,但64位系统的结果非常相似。)

1
不错的基准测试,加一。关于 1a(预分配字符串),实际上你正在丢弃预分配的缓冲区:operator +() 的结果是一个临时对象,被移动(或 RVO'd)到 out 中,因此预分配是无用的。有趣的基准测试将创建 2a / 3a 情况,在这些情况下,您可以提前 reserve() 结果字符串,然后将所有参数 append()+= 到结果字符串中。正如我在我的答案中解释的那样,我曾经进行过这样的基准测试,并得出结论,这确实是最有效的解决方案。 - syam
我拿了你的代码,并添加了一个 build_string_1b 函数,它只是执行 return a + b + c;,在某些运行中(VS2012),这被证明是最快的函数。 - Blastfurnace
1
关于 2a,我只是挑刺一下:目前你有两个内存分配(先复制 a 然后再 reserve),这可以通过在空字符串上使用 reserve,然后 仅仅 += 所有参数来进一步改进(这将为您提供单个内存分配,即 reserve)。我本可以自己编辑,但计时是针对您的机器进行的,所以我让您自己处理。 ;) - syam
@Syam:我最终确实写了那个代码,但我可能复制错了 - 现在发布的是正确的结果。 - Mats Petersson

11

像大多数微优化一样,您需要测量每个选项的效果,并首先通过测量确定这确实是值得优化的瓶颈。没有明确的答案。

append+=应该完全相同。

+在概念上效率较低,因为您正在创建和销毁临时对象。您的编译器可能能够将其优化为与追加一样快。

使用总大小调用reserve可以减少所需的内存分配次数-它们可能是最大的瓶颈。

<<(可能在stringstream上)可能会更快;您需要对此进行测量。如果您需要格式化非字符串类型,则它很有用,但可能不会特别擅长处理字符串。

CString的缺点是它不具有可移植性,像我这样的Unix黑客无法告诉您它的优势可能是什么。


3
我决定使用用户Jesse Good提供的代码进行测试,稍作修改以考虑Rapptz的观察结果,具体而言,每次循环都会构建ostringstream。 因此,我添加了一些情况,其中一对是使用序列“oss.str(“”); oss.clear()”清除的ostringstream。

以下是代码:

#include <iostream>
#include <string>
#include <chrono>
#include <sstream>
#include <functional>


template <typename F> void time_measurement(F f, const std::string& comment)
{
    typedef std::chrono::high_resolution_clock clock;
    typedef std::chrono::duration<float, std::milli> mil;
    std::string r;
    auto t0 = clock::now();
    f(r);
    auto t1 = clock::now();
    std::cout << "\n-------------------------" << comment << "-------------------\n" <<r << '\n';
    std::cout << mil(t1-t0).count() << "ms\n";
    std::cout << "---------------------------------------------------------------------------\n";

}

inline void clear(std::ostringstream& x)
{
    x.str("");
    x.clear();
}

void test()
{
    std:: cout << std::endl << "----------------String Comparison---------------- " << std::endl;
    const int n=100000;
    {
        auto f=[](std::string& l_czTempStr)
        {
            std::string s1="Test data1";
            for (int i = 0; i < n; ++i)
            {
                l_czTempStr = s1 + "Test data2" + "Test data3";
            }
        };
        time_measurement(f, "string, plain addition");
   }

   {
        auto f=[](std::string& l_czTempStr)
        {
            for (int i = 0; i < n; ++i)
            {
                l_czTempStr =  "Test data1";
                l_czTempStr += "Test data2";
                l_czTempStr += "Test data3";
            }
        };
        time_measurement(f, "string, incremental");
    }

    {
         auto f=[](std::string& l_czTempStr)
         {
            for (int i = 0; i < n; ++i)
            {
                l_czTempStr =  "Test data1";
                l_czTempStr.append("Test data2");
                l_czTempStr.append("Test data3");
            }
         };
         time_measurement(f, "string, append");
     }

    {
         auto f=[](std::string& l_czTempStr)
         {
            for (int i = 0; i < n; ++i)
            {
                std::ostringstream oss;
                oss << "Test data1";
                oss << "Test data2";
                oss << "Test data3";
                l_czTempStr = oss.str();
            }
         };
         time_measurement(f, "oss, creation in each loop, incremental");
     }

    {
         auto f=[](std::string& l_czTempStr)
         {
            std::ostringstream oss;
            for (int i = 0; i < n; ++i)
            {
                oss.str("");
                oss.clear();
                oss << "Test data1";
                oss << "Test data2";
                oss << "Test data3";
            }
            l_czTempStr = oss.str();
         };
         time_measurement(f, "oss, 1 creation, incremental");
     }

    {
         auto f=[](std::string& l_czTempStr)
         {
            std::ostringstream oss;
            for (int i = 0; i < n; ++i)
            {
                oss.str("");
                oss.clear();
                oss << "Test data1" << "Test data2" << "Test data3";
            }
            l_czTempStr = oss.str();
         };
         time_measurement(f, "oss, 1 creation, plain addition");
     }

    {
         auto f=[](std::string& l_czTempStr)
         {
            std::ostringstream oss;
            for (int i = 0; i < n; ++i)
            {
                clear(oss);
                oss << "Test data1" << "Test data2" << "Test data3";
            }
            l_czTempStr = oss.str();
         };
         time_measurement(f, "oss, 1 creation, clearing calling inline function, plain addition");
     }


    {
         auto f=[](std::string& l_czTempStr)
         {
            for (int i = 0; i < n; ++i)
            {
                std::string x;
                x =  "Test data1";
                x.append("Test data2");
                x.append("Test data3");
                l_czTempStr=x;
            }
         };
         time_measurement(f, "string, creation in each loop");
     }

}

这是结果:
/*

g++ "qtcreator debug mode"
----------------String Comparison---------------- 

-------------------------string, plain addition-------------------
Test data1Test data2Test data3
11.8496ms
---------------------------------------------------------------------------

-------------------------string, incremental-------------------
Test data1Test data2Test data3
3.55597ms
---------------------------------------------------------------------------

-------------------------string, append-------------------
Test data1Test data2Test data3
3.53099ms
---------------------------------------------------------------------------

-------------------------oss, creation in each loop, incremental-------------------
Test data1Test data2Test data3
58.1577ms
---------------------------------------------------------------------------

-------------------------oss, 1 creation, incremental-------------------
Test data1Test data2Test data3
11.1069ms
---------------------------------------------------------------------------

-------------------------oss, 1 creation, plain addition-------------------
Test data1Test data2Test data3
10.9946ms
---------------------------------------------------------------------------

-------------------------oss, 1 creation, clearing calling inline function, plain addition-------------------
Test data1Test data2Test data3
10.9502ms
---------------------------------------------------------------------------

-------------------------string, creation in each loop-------------------
Test data1Test data2Test data3
9.97495ms
---------------------------------------------------------------------------


g++ "qtcreator release mode" (optimized)
----------------String Comparison----------------

-------------------------string, plain addition-------------------
Test data1Test data2Test data3
8.41622ms
---------------------------------------------------------------------------

-------------------------string, incremental-------------------
Test data1Test data2Test data3
2.55462ms
---------------------------------------------------------------------------

-------------------------string, append-------------------
Test data1Test data2Test data3
2.5154ms
---------------------------------------------------------------------------

-------------------------oss, creation in each loop, incremental-------------------
Test data1Test data2Test data3
54.3232ms
---------------------------------------------------------------------------

-------------------------oss, 1 creation, incremental-------------------
Test data1Test data2Test data3
8.71854ms
---------------------------------------------------------------------------

-------------------------oss, 1 creation, plain addition-------------------
Test data1Test data2Test data3
8.80526ms
---------------------------------------------------------------------------

-------------------------oss, 1 creation, clearing calling inline function, plain addition-------------------
Test data1Test data2Test data3
8.78186ms
---------------------------------------------------------------------------

-------------------------string, creation in each loop-------------------
Test data1Test data2Test data3
8.4034ms
---------------------------------------------------------------------------
*/

现在使用std::string仍然更快,而且append仍然是最快的连接方式,但ostringstream不再像以前那样非常糟糕。

1
#include <concepts>
#include <string>

template<class T>
concept string_like_t = requires(const T & str)
{
    {std::size(str)} -> std::same_as<size_t>;
    {*std::data(str)} -> std::convertible_to<std::remove_cvref_t<decltype(str[0])>>;
};
template<string_like_t T>
using char_t = std::remove_cvref_t<decltype(std::declval<T>()[0])>;

template<class Alloc, string_like_t First, string_like_t... Rest>
    requires (!string_like_t<Alloc>)
auto concat(const Alloc& alloc, const First& first, const Rest&... rest)
{
    std::basic_string<char_t<First>, std::char_traits<char_t<First>>, Alloc> result{ alloc };
    result.reserve(std::size(first) + (std::size(rest) + ...));
    result.append(std::data(first), std::size(first));
    (result.append(std::data(rest), std::size(rest)), ...);
    return result;
}

template<string_like_t First, string_like_t... Rest>
auto concat(const First& first, const Rest&... rest)
{
    typename std::basic_string<char_t<First>>::allocator_type alloc{};
    return concat(alloc, first, rest...);
}

#include <string_view>
#include <iostream>
#include <memory_resource>
int main()
{
    std::pmr::monotonic_buffer_resource mr { 1000 };
    std::pmr::polymorphic_allocator<char> alloc {&mr};
    std::string xxx = "xxxxxx";
    std::string_view yyy = "TEST";
    std::pmr::string zzz {", zzz", &mr};
    std::cout << concat(yyy, "123: ", "test", xxx, zzz) << std::endl;
    std::cout << concat(alloc, yyy, "123: ", "test", xxx, zzz) << std::endl;

    return 0;
}

看起来这是最优化的C++20版本。支持多态分配器。


1
使用C++17,这个简单的解决方案应该有很好的性能,在大多数情况下与@syam的模板重型解决方案相当。在某些条件下,它甚至会更快,避免不必要的strlen调用。
#include <string>
#include <string_view>

template <typename... T>
std::string concat(T ...args) {
    std::string result;
    std::string_view views[] { args... };
    std::string::size_type full_size = 0;
    for (auto sub_view : views)
        full_size += sub_view.size();
    result.reserve(full_size);
    for (auto sub_view : views)
        result.append(sub_view);
    return result;
}

这里有一些冗余 - 我们实际上不需要存储string_views,只需要存储参数的长度。然而,开销微不足道,并且使代码清晰易懂。
std::string_view存储参数的长度。因此,将它们附加到std::string中可能比通过char*附加更快。此外,std::string_view使用std::char_traits进行长度计算,在某些实现中,对于在编译时已知的参数可以在编译时计算。这种优化通常无法针对类似strlen的C调用执行。

考虑使用const T& ...args代替T ...args,以避免在传递左值的情况下产生不必要的参数拷贝。 - Emil

1

由于这个问题的已被接受的答案相当古老,我决定使用现代编译器更新它的基准测试,并将@jesse-good和@syam的两种解决方案进行比较。

以下是合并后的代码:

#include <iostream>
#include <string>
#include <chrono>
#include <sstream>
#include <vector>
#include <cstring>


#if VER==TEMPLATE
namespace detail {

  template<typename>
  struct string_size_impl;

  template<size_t N>
  struct string_size_impl<const char[N]> {
    static constexpr size_t size(const char (&) [N]) { return N - 1; }
  };

  template<size_t N>
  struct string_size_impl<char[N]> {
    static size_t size(char (&s) [N]) { return N ? strlen(s) : 0; }
  };

  template<>
  struct string_size_impl<const char*> {
    static size_t size(const char* s) { return s ? strlen(s) : 0; }
  };

  template<>
  struct string_size_impl<char*> {
    static size_t size(char* s) { return s ? strlen(s) : 0; }
  };

  template<>
  struct string_size_impl<std::string> {
    static size_t size(const std::string& s) { return s.size(); }
  };

  template<typename String> size_t string_size(String&& s) {
    using noref_t = typename std::remove_reference<String>::type;
    using string_t = typename std::conditional<std::is_array<noref_t>::value,
                                              noref_t,
                                              typename std::remove_cv<noref_t>::type
                                              >::type;
    return string_size_impl<string_t>::size(s);
  }

  template<typename...>
  struct concatenate_impl;

  template<typename String>
  struct concatenate_impl<String> {
    static size_t size(String&& s) { return string_size(s); }
    static void concatenate(std::string& result, String&& s) { result += s; }
  };

  template<typename String, typename... Rest>
  struct concatenate_impl<String, Rest...> {
    static size_t size(String&& s, Rest&&... rest) {
      return string_size(s)
           + concatenate_impl<Rest...>::size(std::forward<Rest>(rest)...);
    }
    static void concatenate(std::string& result, String&& s, Rest&&... rest) {
      result += s;
      concatenate_impl<Rest...>::concatenate(result, std::forward<Rest>(rest)...);
    }
  };

} // namespace detail

template<typename... Strings>
std::string concatenate(Strings&&... strings) {
  std::string result;
  result.reserve(detail::concatenate_impl<Strings...>::size(std::forward<Strings>(strings)...));
  detail::concatenate_impl<Strings...>::concatenate(result, std::forward<Strings>(strings)...);
  return result;
}

#endif

int main ()
{
typedef std::chrono::high_resolution_clock clock;
typedef std::chrono::duration<float, std::milli> ms;
std::string l_czTempStr;


std::string s1="Test data1";


auto t0 = clock::now();
#if VER==PLUS
for (int i = 0; i < 100000; ++i)
{
    l_czTempStr = s1 + "Test data2" + "Test data3";
}
#elif VER==PLUS_EQ
for (int i = 0; i < 100000; ++i)
{
    l_czTempStr =  "Test data1"; 
    l_czTempStr += "Test data2";
    l_czTempStr += "Test data3";
}
#elif VER==APPEND
for (int i = 0; i < 100000; ++i)
{
    l_czTempStr =  "Test data1"; 
    l_czTempStr.append("Test data2");
    l_czTempStr.append("Test data3");
}
#elif VER==STRSTREAM
for (int i = 0; i < 100000; ++i)
{
    std::ostringstream oss;
    oss << "Test data1";
    oss << "Test data2";
    oss << "Test data3";
    l_czTempStr = oss.str();
}
#elif VER=TEMPLATE
for (int i = 0; i < 100000; ++i)
{
    l_czTempStr = concatenate(s1, "Test data2", "Test data3");
}
#endif

#define STR_(x) #x
#define STR(x) STR_(x)

auto t1 = clock::now();
//std::cout << l_czTempStr << '\n';
std::cout << STR(VER) ": " << ms(t1-t0).count() << "ms\n";
}

测试指令:

for ARGTYPE in PLUS PLUS_EQ APPEND STRSTREAM TEMPLATE; do for i in `seq 4` ; do clang++ -std=c++11 -O3 -DVER=$ARGTYPE -Wall -pthread -pedantic main.cpp && ./a.out ; rm ./a.out ; done; done

以下是结果(通过电子表格处理以显示平均时间):

PLUS       23.5792   
PLUS       23.3812   
PLUS       35.1806   
PLUS       15.9394   24.5201
PLUS_EQ    15.737    
PLUS_EQ    15.3353   
PLUS_EQ    10.7764   
PLUS_EQ    25.245    16.773425
APPEND     22.954    
APPEND     16.9031   
APPEND     10.336    
APPEND     19.1348   17.331975
STRSTREAM  10.2063   
STRSTREAM  10.7765   
STRSTREAM  13.262    
STRSTREAM  22.3557   14.150125
TEMPLATE   16.6531   
TEMPLATE   16.629    
TEMPLATE   22.1885   
TEMPLATE   16.9288   18.09985

意外的是,strstream 在 C++11 及以后的改进中似乎受益匪浅。可能由于引入移动语义而消除了必要的分配,这产生了一些影响。

您可以在 coliru 上自行测试。

编辑: 我已更新 coliru 上的测试使用 g++-4.8:http://coliru.stacked-crooked.com/a/593dcfe54e70e409。结果在此处的图表中显示: g++-4.8 test results

(说明 - “stat. average” 表示除两个极值之外的所有值的平均值 - 一个最小值和一个最大值)


1
值得注意的是,YMMV,并且您应该执行自己的基准测试,因为我的编译针对g ++ 4.8.5进行,启用了各种其他标志,导致流性能大约是+=和append的两倍,以及比+高50%的增加。 - devyndraen
@devyndraen - 百分之百正确 - 每个方面都很重要。然而,2倍仍然比Jesse Good最初报告的约20倍要好得多。顺便说一下,这是我(4次运行的总和)使用g++-7的结果:PLUS:18.49352,PLUS_EQ:19.03214,APPEND:18.70595,STRSTREAM:19.17043,TEMPLATE:21.98324。 - yatsek
尝试使用g++-4.8在Coliru上重复测试:http://coliru.stacked-crooked.com/a/593dcfe54e70e409,图表如下:https://i.imgur.com/w4TXPO3.png(stat.avg.是所有最小值和最大值的平均值) - yatsek
我也在我的系统上尝试了你的代码,惊讶地发现与旧代码相比,数值差异如此之大。 看起来ifdef没有正常工作。这就是结果如此接近的原因。 将其改回数字,并在每个ifdef下添加适当的打印,我得到了以下结果:PLUS: 1: 2.20834毫秒,PLUS_EQ: 2: 0.340278毫秒,APPEND: 3: 0.310837毫秒,STRSTREAM: 4: 10.5987毫秒,TEMPLATE: 5: 2.12325毫秒gcc-13.2/Ryzen5950X如果stringstream能迎头赶上就好了...但看起来似乎没有。 - undefined

0

有一些重要的参数可能会影响决定“最优化方式”。其中一些是-字符串/内容大小,操作数量,编译器优化等。

在大多数情况下,string :: operator + =似乎效果最佳。然而,有时候在某些编译器上,也观察到ostringstream :: operator << 效果最佳[例如- MingW g ++ 3.2.3,1.8 GHz单处理器Dell PC ]。当涉及编译器上下文时,主要是编译器优化会产生影响。还要提到的是,与简单字符串相比,stringstreams 是复杂对象,因此增加了开销。

有关详细信息- discussion, article


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接