详解C++中StringBuilder类的实现及其性能优化

2020-01-06 15:08:05于海丽


函数ToString()使用std::string::reserve()来实现最小化再分配。下面你可以看到一个性能测试的结果。
函数join()使用std::accumulate(),和一个已经为首个操作数预留内存的自定义函数。
你可能会问,为什么StringBuilder::m_Data用std::list而不是std::vector?除非你有一个用其他容器的好理由,通常都是使用std::vector。
好吧,我(这样做)有两个原因:
1. 字符串总是会附加到一个容器的末尾。std::list允许在不需要内存再分配的情况下这样做;因为vector是使用一个连续的内存块实现的,每用一个就可能导致内存再分配。
2. std::list对顺序存取相当有利,而且在m_Data上所做的唯一存取操作也是顺序的。
你可以建议同时测试这两种实现的性能和内存占用情况,然后选择其中一个。

性能评估

为了测试性能,我从Wikipedia获取一个网页,并将其中一部分内容写死到一个string的vector中。
随后,我编写两个测试函数,第一个在两个循环中使用标准函数clock()并调用std::accumulate()和StringBuilder::ToString(),然后打印结果。


void TestPerformance(const StringBuilder<wchar_t> &tested, const std::vector<std::wstring> &tested2) {
  const int loops = 500;
  clock_t start = clock(); // Give up some accuracy in exchange for platform independence.
  for (int i = 0; i < loops; ++i) {
    std::wstring accumulator;
    std::accumulate(tested2.begin(), tested2.end(), accumulator);
  }
  double secsAccumulate = (double) (clock() - start) / CLOCKS_PER_SEC;
 
  start = clock();
  for (int i = 0; i < loops; ++i) {
    std::wstring result2 = tested.ToString();
  }
  double secsBuilder = (double) (clock() - start) / CLOCKS_PER_SEC;
  using std::cout;
  using std::endl;
  cout << "Accumulate took " << secsAccumulate << " seconds, and ToString() took " << secsBuilder << " seconds."
      << " The relative speed improvement was " << ((secsAccumulate / secsBuilder) - 1) * 100 << "%"
      << endl;
}

第二个则使用更精确的Posix函数clock_gettime(),并测试StringBuilder::Join()。


#ifdef __USE_POSIX199309
 
// Thanks to <a href="http://www.easck.com/2007/09/22/profiling-code-using-clock_gettime/">Guy Rutenberg</a>.
timespec diff(timespec start, timespec end)
{
  timespec temp;
  if ((end.tv_nsec-start.tv_nsec)<0) {
    temp.tv_sec = end.tv_sec-start.tv_sec-1;
    temp.tv_nsec = 1000000000+end.tv_nsec-start.tv_nsec;
  } else {
    temp.tv_sec = end.tv_sec-start.tv_sec;
    temp.tv_nsec = end.tv_nsec-start.tv_nsec;
  }
  return temp;
}
 
void AccurateTestPerformance(const StringBuilder<wchar_t> &tested, const std::vector<std::wstring> &tested2) {
  const int loops = 500;
  timespec time1, time2;
  // Don't forget to add -lrt to the g++ linker command line.
  ////////////////
  // Test std::accumulate()
  ////////////////
  clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time1);
  for (int i = 0; i < loops; ++i) {
    std::wstring accumulator;
    std::accumulate(tested2.begin(), tested2.end(), accumulator);
  }
  clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time2);
  using std::cout;
  using std::endl;
  timespec tsAccumulate =diff(time1,time2);
  cout << tsAccumulate.tv_sec << ":" << tsAccumulate.tv_nsec << endl;
  ////////////////
  // Test ToString()
  ////////////////
  clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time1);
  for (int i = 0; i < loops; ++i) {
    std::wstring result2 = tested.ToString();
  }
  clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time2);
  timespec tsToString =diff(time1,time2);
  cout << tsToString.tv_sec << ":" << tsToString.tv_nsec << endl;
  ////////////////
  // Test join()
  ////////////////
  clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time1);
  for (int i = 0; i < loops; ++i) {
    std::wstring result3 = tested.Join(L",");
  }
  clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time2);
  timespec tsJoin =diff(time1,time2);
  cout << tsJoin.tv_sec << ":" << tsJoin.tv_nsec << endl;
 
  ////////////////
  // Show results
  ////////////////
  double secsAccumulate = tsAccumulate.tv_sec + tsAccumulate.tv_nsec / 1000000000.0;
  double secsBuilder = tsToString.tv_sec + tsToString.tv_nsec / 1000000000.0;
    double secsJoin = tsJoin.tv_sec + tsJoin.tv_nsec / 1000000000.0;
  cout << "Accurate performance test:" << endl << "  Accumulate took " << secsAccumulate << " seconds, and ToString() took " << secsBuilder << " seconds." << endl
      << "  The relative speed improvement was " << ((secsAccumulate / secsBuilder) - 1) * 100 << "%" << endl <<
       "   Join took " << secsJoin << " seconds."
      << endl;
}
#endif // def __USE_POSIX199309