C++에서 StringBuilder 류 의 실현 과 성능 최적화
14374 단어 C++StringBuilder
클 라 이언 트 들 이 달팽이 처럼 프로그램 이 느리다 고 불평 하 는 경우 가 많다.가능 한 의문점 을 검사 하기 시작 합 니 다:파일 IO,데이터베이스 접근 속도,심지어 웹 서 비 스 를 봅 니 다.그러나 이 가능 한 의문점 들 은 모두 정상 적 이 고 전혀 문제 가 없다.
가장 손 쉬 운 성능 분석 도 구 를 사용 하여 분석 한 결과 병목 은 작은 함수 에 있 습 니 다.이 함수 의 역할 은 긴 문자열 링크 를 파일 에 쓰 는 것 입 니 다.
이 함수 에 대해 다음 과 같은 최 적 화 를 했 습 니 다.모든 작은 문자열 을 긴 문자열 로 연결 하고 파일 기록 작업 을 수행 하여 수천 번 의 작은 문자열 이 파일 을 쓰 는 것 을 피 합 니 다.
이 최 적 화 는 반 만 맞 췄 다.
먼저 큰 문자열 로 파일 을 쓰 는 속 도 를 테스트 해 보 세 요.번개 처럼 빠 릅 니 다.그리고 모든 문자열 의 연결 속 도 를 테스트 하 세 요.
여러 해.
어떻게 된 거 야?당신 은 이 문 제 를 어떻게 극복 할 것 입 니까?
.net 프로그래머 가 StringBuilder 를 사용 하여 이 문 제 를 해결 할 수 있다 는 것 을 알 고 있 을 지도 모른다.이것 도 본문의 출발점 이다.
배경
구 글 이'C++StringBuilder'를 사용 하면 많은 답 을 얻 을 수 있 습 니 다.std::accumulate 를 사용 하 는 것 을 권장 합 니 다.이것 은 당신 이 실현 하고 자 하 는 거의 모든 것 을 완성 할 수 있 습 니 다.
#include <iostream>// for std::cout, std::endl
#include <string> // for std::string
#include <vector> // for std::vector
#include <numeric> // for std::accumulate
int main()
{
using namespace std;
vector<string> vec = { "hello", " ", "world" };
string s = accumulate(vec.begin(), vec.end(), s);
cout << s << endl; // prints 'hello world' to standard output.
return 0;
}
지금까지 모든 것 이 좋 았 습 니 다.몇 개의 문자열 이 연결 되 어 있 을 때 문제 가 발생 했 고 메모리 재분배 도 쌓 이기 시 작 했 습 니 다.std::string 은 함수 reserver()에서 해결 방안 에 기 초 를 제공 합 니 다.이것 도 바로 우리 의 의도 이다.한 번 의 분배,마음대로 연결 하 는 것 이다.
문자열 연결 은 무 겁 고 둔 한 도구 로 인해 성능 에 심각 한 영향 을 줄 수 있 습 니 다.지난번 에 존 재 했 던 위험 으로 인해 이 특수 한 괴짜 가 나 에 게 문 제 를 일 으 켰 기 때문에 나 는 인디고 를 포기 했다.(나 는 C++11 의 새로운 특성 을 시도 하고 싶다)그리고 StringBuilder 류 의 일부 실현 을 썼 다.
// Subset of http://msdn.microsoft.com/en-us/library/system.text.stringbuilder.aspx
template <typename chr>
class StringBuilder {
typedef std::basic_string<chr> string_t;
typedef std::list<string_t> container_t; // Reasons not to use vector below.
typedef typename string_t::size_type size_type; // Reuse the size type in the string.
container_t m_Data;
size_type m_totalSize;
void append(const string_t &src) {
m_Data.push_back(src);
m_totalSize += src.size();
}
// No copy constructor, no assignement.
StringBuilder(const StringBuilder &);
StringBuilder & operator = (const StringBuilder &);
public:
StringBuilder(const string_t &src) {
if (!src.empty()) {
m_Data.push_back(src);
}
m_totalSize = src.size();
}
StringBuilder() {
m_totalSize = 0;
}
// TODO: Constructor that takes an array of strings.
StringBuilder & Append(const string_t &src) {
append(src);
return *this; // allow chaining.
}
// This one lets you add any STL container to the string builder.
template<class inputIterator>
StringBuilder & Add(const inputIterator &first, const inputIterator &afterLast) {
// std::for_each and a lambda look like overkill here.
// <b>Not</b> using std::copy, since we want to update m_totalSize too.
for (inputIterator f = first; f != afterLast; ++f) {
append(*f);
}
return *this; // allow chaining.
}
StringBuilder & AppendLine(const string_t &src) {
static chr lineFeed[] { 10, 0 }; // C++ 11. Feel the love!
m_Data.push_back(src + lineFeed);
m_totalSize += 1 + src.size();
return *this; // allow chaining.
}
StringBuilder & AppendLine() {
static chr lineFeed[] { 10, 0 };
m_Data.push_back(lineFeed);
++m_totalSize;
return *this; // allow chaining.
}
// TODO: AppendFormat implementation. Not relevant for the article.
// Like C# StringBuilder.ToString()
// Note the use of reserve() to avoid reallocations.
string_t ToString() const {
string_t result;
// The whole point of the exercise!
// If the container has a lot of strings, reallocation (each time the result grows) will take a serious toll,
// both in performance and chances of failure.
// I measured (in code I cannot publish) fractions of a second using 'reserve', and almost two minutes using +=.
result.reserve(m_totalSize + 1);
// result = std::accumulate(m_Data.begin(), m_Data.end(), result); // This would lose the advantage of 'reserve'
for (auto iter = m_Data.begin(); iter != m_Data.end(); ++iter) {
result += *iter;
}
return result;
}
// like javascript Array.join()
string_t Join(const string_t &delim) const {
if (delim.empty()) {
return ToString();
}
string_t result;
if (m_Data.empty()) {
return result;
}
// Hope we don't overflow the size type.
size_type st = (delim.size() * (m_Data.size() - 1)) + m_totalSize + 1;
result.reserve(st);
// If you need reasons to love C++11, here is one.
struct adder {
string_t m_Joiner;
adder(const string_t &s): m_Joiner(s) {
// This constructor is NOT empty.
}
// This functor runs under accumulate() without reallocations, if 'l' has reserved enough memory.
string_t operator()(string_t &l, const string_t &r) {
l += m_Joiner;
l += r;
return l;
}
} adr(delim);
auto iter = m_Data.begin();
// Skip the delimiter before the first element in the container.
result += *iter;
return std::accumulate(++iter, m_Data.end(), result, adr);
}
}; // class StringBuilder
함수 ToString()은 std::string::reserve()를 사용 하여 최소 화 재 분 배 를 실현 합 니 다.다음은 성능 테스트 결 과 를 볼 수 있다.함수 join()은 std:accumulate()를 사용 하고 첫 번 째 조작 수 에 메모 리 를 미리 남 겨 둔 사용자 정의 함수 입 니 다.
왜 StringBuilder::mdata 는 std::list 가 아 닌 std::vector 를 사용 합 니까?다른 용 기 를 사용 할 좋 은 이유 가 있 는 경 우 를 제외 하고 보통 std:vector 를 사용 합 니 다.
좋아,나 는 두 가지 이유 가 있어.
1.문자열 은 항상 용기 의 끝 에 추 가 됩 니 다.std::list 는 메모리 재분배 가 필요 없 는 상황 에서 이렇게 할 수 있 습 니 다.vector 는 연속 적 인 메모리 블록 을 사용 하여 이 루어 지기 때문에 하 나 를 사용 할 때마다 메모리 재 분 배 를 초래 할 수 있 습 니 다.
2.std::list 는 순차 액세스 에 상당히 유리 하고 mData 에서 하 는 유일한 액세스 작업 도 순서 입 니 다.
이 두 가지 실 현 된 성능 과 메모리 점용 상황 을 동시에 테스트 한 후에 그 중 하 나 를 선택 하 는 것 을 건의 할 수 있다.
성능 평가
성능 을 테스트 하기 위해 서,나 는 위 키 백과 에서 웹 페이지 를 얻 었 고,그 중 일 부 를 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.guyrutenberg.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
마지막 으로 하나의 main 함 수 를 통 해 상기 실 현 된 두 함 수 를 호출 하여 결 과 를 콘 솔 에 표시 한 다음 성능 테스트 를 실행 합 니 다.하 나 는 디 버 깅 설정 에 사 용 됩 니 다.다른 버 전:
이 백분율 봤 어?스 팸 메 일의 발 송 량 이 이 단계 에 이 르 지 못 합 니 다!
코드 사용
이 코드 를 사용 하기 전에 ostring 흐름 을 사용 하 는 것 을 고려 합 니 다.제 프 씨 의 논평 을 아래 에서 보 았 듯 이,그것 은 이 글 의 코드 보다 좀 빠르다.
이 코드 를 사용 하고 싶 을 수도 있 습 니 다.만약:
C\#경험 이 있 는 프로그래머 가 관리 하 는 코드 를 만 들 고 있 으 며,인터페이스 에 익숙 한 코드 를 제공 하고 싶 습 니 다.
앞으로.net 으로 전환 할 코드 를 만 들 고 있 습 니 다.가능 한 경 로 를 알려 주 고 싶 습 니 다.
어떤 이유 로,당신 은
이 코드 를 사용 하려 면 main 함수 에 따라 만 가능 합 니 다.StringBuilder 의 인 스 턴 스 를 만 들 고 Append(),AppendLine(),Add()로 값 을 부여 한 다음 ToString 함수 검색 결 과 를 호출 합 니 다.
아래 처럼:
int main() {
////////////////////////////////////
// 8-bit characters (ANSI)
////////////////////////////////////
StringBuilder<char> ansi;
ansi.Append("Hello").Append(" ").AppendLine("World");
std::cout << ansi.ToString();
////////////////////////////////////
// Wide characters (Unicode)
////////////////////////////////////
// http://en.wikipedia.org/wiki/Cargo_cult
std::vector<std::wstring> cargoCult
{
L"A", L" cargo", L" cult", L" is", L" a", L" kind", L" of", L" Melanesian", L" millenarian", L" movement",
// many more lines here...
L" applied", L" retroactively", L" to", L" movements", L" in", L" a", L" much", L" earlier", L" era.
"
};
StringBuilder<wchar_t> wide;
wide.Add(cargoCult.begin(), cargoCult.end()).AppendLine();
// use ToString(), just like .net
std::wcout << wide.ToString() << std::endl;
// javascript-like join.
std::wcout << wide.Join(L" _
") << std::endl;
////////////////////////////////////
// Performance tests
////////////////////////////////////
TestPerformance(wide, cargoCult);
#ifdef __USE_POSIX199309
AccurateTestPerformance(wide, cargoCult);
#endif // def __USE_POSIX199309
return 0;
}
어떤 경우 에 도 연결 이 몇 개의 문자열 을 초과 할 때 std:accumulate 함 수 를 조심 하 십시오.지금 잠깐 만!
당신 은 우리 가 미리 최적화 하도록 설득 하려 고 하 는 것 입 니까?라 고 물 을 수 있 습 니 다.
아 닙 니 다.나 는 앞 당 겨 최적화 하 는 것 이 나 쁜 것 에 찬성한다.이런 최 적 화 는 결코 앞 당 긴 것 이 아니다.제때에 하 는 것 이다.이것 은 경험 을 바탕 으로 하 는 최적화 이다.나 는 내 가 과거 에 이런 특수 한 괴물 과 싸 웠 다 는 것 을 알 게 되 었 다.경험 에 기초 한 최적화(같은 곳 에서 두 번 넘 어 지지 않 음)는 미리 최적화 하 는 것 이 아니다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
Visual Studio에서 파일 폴더 구분 (포함 경로 설정)Visual Studio에서 c, cpp, h, hpp 파일을 폴더로 나누고 싶었습니까? 어쩌면 대부분의 사람들이 있다고 생각합니다. 처음에 파일이 만들어지는 장소는 프로젝트 파일 등과 같은 장소에 있기 때문에 파일...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.