C ++에서 효율적인 문자열 연결


108

std :: string의 "+"연산자와 연결 속도를 높이기위한 다양한 해결 방법에 대한 걱정을 표현하는 사람이 몇 명 있다고 들었습니다. 이 중 정말로 필요한 것이 있습니까? 그렇다면 C ++에서 문자열을 연결하는 가장 좋은 방법은 무엇입니까?


13
기본적으로 +는 연결 연산자가 아닙니다 (새 문자열을 생성하므로). 연결에는 + =를 사용하십시오.
Martin York

1
C ++ 11부터 중요한 점이 있습니다. operator +는 해당 피연산자 중 하나를 수정하고 해당 피연산자가 rvalue 참조로 전달 된 경우 이동별로 반환 할 수 있습니다. libstdc++ 예를 들어 . 따라서 임시로 operator +를 호출 할 때 거의 좋은 성능을 얻을 수 있습니다. 아마도 병목 현상을 보여주는 벤치 마크가없는 한 가독성을 위해 기본값을 선호하는 주장 일 것입니다. 그러나 표준화 된 variadic append()은 최적 이고 읽기 가능합니다 ...
underscore_d

답변:


85

정말로 효율성이 필요한 경우가 아니라면 추가 작업은 그만한 가치가 없습니다. 대신 연산자 + =를 사용하면 훨씬 더 나은 효율성을 얻을 수 있습니다.

이제 그 면책 조항 이후에 실제 질문에 대답하겠습니다 ...

STL 문자열 클래스의 효율성은 사용중인 STL 구현에 따라 다릅니다.

당신은 수있는 효율성을 보장 하고 더 많은 컨트롤을 가지고 내장 함수 c를 통해 수동으로 연결을 수행하여 자신을.

operator +가 효율적이지 않은 이유 :

이 인터페이스를 살펴보십시오.

template <class charT, class traits, class Alloc>
basic_string<charT, traits, Alloc>
operator+(const basic_string<charT, traits, Alloc>& s1,
          const basic_string<charT, traits, Alloc>& s2)

각 + 다음에 새 개체가 반환되는 것을 볼 수 있습니다. 즉, 매번 새 버퍼가 사용됩니다. 많은 추가 + 작업을 수행하는 경우 효율적이지 않습니다.

더 효율적으로 만들 수있는 이유 :

  • 대리인이 귀하를 위해 효율적으로 수행하도록 신뢰하는 대신 효율성을 보장합니다.
  • std :: string 클래스는 문자열의 최대 크기 나 연결 빈도에 대해 전혀 알지 못합니다. 당신은이 지식을 가지고 있고이 정보를 가지고 일을 할 수 있습니다. 이렇게하면 재 할당이 줄어 듭니다.
  • 버퍼를 수동으로 제어하여 전체 문자열을 새 버퍼에 복사하지 않도록 할 수 있습니다.
  • 훨씬 더 효율적인 힙 대신 버퍼에 스택을 사용할 수 있습니다.
  • string + 연산자는 새 문자열 객체를 만들고이를 반환하므로 새 버퍼를 사용합니다.

구현 고려 사항 :

  • 문자열 길이를 추적하십시오.
  • 문자열의 끝과 시작 또는 시작 부분에 대한 포인터를 유지하고 시작 + 길이를 오프셋으로 사용하여 문자열의 끝을 찾습니다.
  • 문자열을 저장하는 버퍼가 충분히 큰지 확인하여 데이터를 다시 할당 할 필요가 없습니다.
  • strcat 대신 strcpy를 사용하면 문자열의 끝을 찾기 위해 문자열 길이를 반복 할 필요가 없습니다.

로프 데이터 구조 :

정말 빠른 연결이 필요한 경우 로프 데이터 구조 사용을 고려하십시오 .


6
참고 : "STL"은 원래 HP에서 제공하는 완전히 별개의 오픈 소스 라이브러리를 의미하며, 일부는 ISO 표준 C ++ 라이브러리의 일부에 대한 기반으로 사용되었습니다. 그러나 "std :: string"은 HP STL의 일부가 아니므로 "STL과"string "을 함께 참조하는 것은 완전히 잘못되었습니다.
James Curran

1
나는 STL과 문자열을 함께 사용하는 것이 잘못이라고 말하지 않을 것입니다. sgi.com/tech/stl/table_of_contents.html
Brian R. Bondy

1
SGI가 HP에서 STL의 유지 보수를 인수했을 때 표준 라이브러리와 일치하도록 개조되었습니다 (이것이 제가 "HP STL의 일부가 아닙니다"라고 말한 이유입니다). 그럼에도 불구하고 std :: string의 창시자는 ISO C ++위원회입니다.
James Curran

2
참고 : 수년간 STL을 유지 관리하는 SGI 직원은 Matt Austern이었으며 동시에 ISO C ++ 표준화위원회의 라이브러리 하위 그룹을 이끌었습니다.
James Curran

4
훨씬 더 효율적인 힙 대신 버퍼에 스택을 사용할 수있는 이유를 설명하거나 몇 가지 요점을 알려 주시겠습니까 ? ? 이 효율성 차이는 어디에서 비롯됩니까?
h7r

76

이전에 최종 공간을 예약 한 다음 버퍼와 함께 append 메소드를 사용하십시오. 예를 들어, 최종 문자열 길이가 1 백만 자일 것으로 예상한다고 가정합니다.

std::string s;
s.reserve(1000000);

while (whatever)
{
  s.append(buf,len);
}

17

나는 그것에 대해 걱정하지 않을 것입니다. 루프에서 수행하면 문자열은 항상 메모리를 미리 할당하여 재 할당을 최소화 operator+=합니다.이 경우 에만 사용하십시오 . 수동으로하면 이런 식 이상이

a + " : " + c

그런 다음 컴파일러가 일부 반환 값 복사본을 제거 할 수 있더라도 임시 파일을 만듭니다. 연속적으로 호출 operator+되면 참조 매개 변수가 명명 된 객체를 참조하는지 아니면 하위 operator+호출 에서 반환 된 임시를 참조하는지 알지 못 하기 때문입니다 . 나는 먼저 프로파일 링하지 않기 전에 그것에 대해 걱정하지 않을 것입니다. 그러나 그것을 보여주는 예를 들어 봅시다. 바인딩을 명확하게하기 위해 먼저 괄호를 도입합니다. 명확성을 위해 사용되는 함수 선언 바로 뒤에 인수를 넣었습니다. 그 아래에서 결과 표현식이 무엇인지 보여줍니다.

((a + " : ") + c) 
calls string operator+(string const&, char const*)(a, " : ")
  => (tmp1 + c)

이제 그 추가 tmp1에서 표시된 인수를 사용하여 operator +에 대한 첫 번째 호출에서 반환 된 것입니다. 컴파일러가 정말 영리하고 반환 값 복사를 최적화한다고 가정합니다. 그래서 우리는 a및 의 연결을 포함하는 하나의 새로운 문자열로 끝납니다 " : ". 이제 이런 일이 발생합니다.

(tmp1 + c)
calls string operator+(string const&, string const&)(tmp1, c)
  => tmp2 == <end result>

다음과 비교하십시오.

std::string f = "hello";
(f + c)
calls string operator+(string const&, string const&)(f, c)
  => tmp1 == <end result>

임시 및 명명 된 문자열에 대해 동일한 기능을 사용하고 있습니다! 따라서 컴파일러 인수를 새 문자열에 복사하고 여기에 추가 한 다음 operator+. 그것은 일시적인 기억을 가지고 그것에 추가 할 수 없습니다. 표현식이 클수록 더 많은 문자열을 복사해야합니다.

다음 Visual Studio 및 GCC는 실험적 추가 기능으로 c ++ 1x의 이동 의미 체계 ( 복사 의미 체계 보완 ) 및 rvalue 참조를 지원합니다. 이를 통해 매개 변수가 임시를 참조하는지 여부를 파악할 수 있습니다. 위의 모든 내용이 복사본없이 하나의 "파이프 라인 추가"로 끝나기 때문에 이러한 추가 작업은 놀랍도록 빠르게 이루어집니다.

병목 현상으로 판명되면 여전히 할 수 있습니다.

 std::string(a).append(" : ").append(c) ...

append호출에 인수를 추가 *this하고 자신에 대한 참조를 반환합니다. 따라서 임시 복사가 수행되지 않습니다. 또는를 operator+=사용할 수 있지만 우선 순위를 수정하려면보기 흉한 괄호가 필요합니다.


stdlib 구현자가 실제로 이것을하는지 확인해야했습니다. : P libstdc++for operator+(string const& lhs, string&& rhs)does return std::move(rhs.insert(0, lhs)). 그런 다음 둘 다 임시 인 operator+(string&& lhs, string&& rhs)경우 lhs사용 가능한 충분한 용량 이 있으면 직접 append(). 용량이 충분하지 않은 operator+=경우 보다 느려질 위험이 있다고 생각하는 곳 lhs은으로 돌아갑니다 rhs.insert(0, lhs). 버퍼를 확장하고 새로운 내용을 추가해야 할 append()뿐만 아니라 rhs오른쪽 의 원래 내용을 따라 이동해야합니다 .
underscore_d

비교되는 또 다른 오버 헤드 operator+=operator+여전히 값을 반환해야하므로 move()추가 된 피연산자에 있어야 한다는 것입니다. 그래도 전체 문자열을 딥 복사하는 것에 비해 상당히 작은 오버 헤드 (포인터 / 크기 복사)가 상당히 적다고 생각합니다.
underscore_d

11

대부분의 응용 프로그램에서는 중요하지 않습니다. + 연산자가 정확히 어떻게 작동하는지 알지 못한 채 코드를 작성하고 병목 현상이 명백한 경우에만 문제를 직접 처리하십시오.


7
물론 대부분의 경우에는 그럴 가치가 없지만 이것이 그의 질문에 실제로 대답하지는 않습니다.
Brian R. Bondy

1
네. 나는 단지 "프로필을 최적화 한 다음 최적화"라는 말을 질문에 대한 코멘트로 넣을 수 있다는 것에 동의합니다. :)
Johannes Schaub-litb

6
기술적으로 그는 이것이 "필요한지"물었다. 그렇지 않습니다. 이것은 그 질문에 대한 답입니다.
Samantha Branham

충분히 공평하지만 일부 응용 프로그램에는 확실히 필요합니다. 그래서 그 대답에 감소 애플리케이션에서 '자신의 손에 걸릴 문제'
브라이언 R. 본디

4
@Pesto 프로그래밍 세계에는 성능이 중요하지 않으며 컴퓨터가 계속 빨라지기 때문에 전체 거래를 무시할 수 있다는 왜곡 된 개념이 있습니다. 문제는 사람들이 C ++로 프로그래밍하는 이유가 아니며 효율적인 문자열 연결에 대한 질문을 스택 오버플로에 게시하는 이유도 아닙니다.
MrFox

7

.NET System.Strings와 달리 C ++의 std :: strings 변경 가능하므로 다른 메서드와 마찬가지로 간단한 연결을 통해 빌드 할 수 있습니다.


2
특히 시작하기 전에 결과에 대해 충분히 큰 버퍼를 만들기 위해 reserve ()를 사용하는 경우.
Mark Ransom

나는 그가 operator + =에 대해 이야기하고 있다고 생각합니다. 그것은 퇴화되는 경우이지만 연결하고 있습니다. P : 나는 그가 ++ C의 몇 가지 단서가 기대 때문에 제임스는 VC ++ MVP이었다
요하네스 SCHAUB - litb

1
나는 그가 C ++에 대한 광범위한 지식을 가지고 있다는 것을 잠시 의심하지 않고 단지 질문에 대한 오해가 있었다. 이 질문은 호출 될 때마다 새 문자열 객체를 반환하므로 새 char 버퍼를 사용하는 operator +의 효율성에 대해 질문했습니다.
Brian R. Bondy

1
네. 그러나 그는 case operator +가 느립니다. 가장 좋은 방법은 연결을 수행하는 것입니다. 여기서 operator + =가 게임에 들어옵니다. 하지만 제임스의 대답이 약간 짧다는 데 동의합니다. 우리 모두가 operator +를 사용할 수있는 것처럼
들리며

@ BrianR.Bondy operator+는 새 문자열을 반환 할 필요가 없습니다. 구현자는 피연산자가 rvalue 참조에 의해 전달 된 경우 수정 된 피연산자 중 하나를 반환 할 수 있습니다. libstdc++ 예를 들어 . 따라서 operator+임시로 호출 할 때 동일한 또는 거의 동일한 성능을 달성 할 수 있습니다. 이는 병목 현상을 나타내는 벤치 마크가없는 한 기본 설정을 선호하는 또 다른 주장 일 수 있습니다.
underscore_d


4

에서는 불완전 C ++ , 매튜 윌슨 제시 동적 모든 부품을 연결하기 전에 하나의 할당을하기 위해 최종 문자열의 길이가 사전 - 계산하는 것을 문자열 연접한다. 표현식 템플릿을 사용 하여 정적 연결자를 구현할 수도 있습니다 .

그런 종류의 아이디어는 STLport std :: string 구현에서 구현되었습니다.이 정확한 해킹 때문에 표준을 따르지 않습니다.


Glib::ustring::compose()glibmm 바인딩에서 GLib로이 작업을 수행합니다. reserve()제공된 형식 문자열과 varargs를 기반으로 최종 길이를 추정하고 s 한 다음 append()루프에서 각각 (또는 형식화 된 대체)을 수행합니다. 나는 이것이 꽤 일반적인 작업 방식이라고 생각합니다.
underscore_d

4

std::string operator+새 문자열을 할당하고 매번 두 개의 피연산자 문자열을 복사합니다. 여러 번 반복하면 비용이 많이 듭니다. O (n).

std::string append그리고 operator+=다른 한편으로는, 50 % 문자열 성장을 필요로 할 때마다 용량을 범프. 메모리 할당 및 복사 작업 수를 크게 줄입니다. O (log n).


왜 이것이 반대표를 받았는지 잘 모르겠습니다. 50 % 수치는 표준에서 요구하지 않지만 IIRC 또는 100 %는 실제 성장의 일반적인 척도입니다. 이 답변의 다른 모든 것은 반대 할 수없는 것 같습니다.
underscore_d

몇 달 후, C ++ 11이 데뷔 한 지 한참 후에 작성되었고 operator+, 하나 또는 두 개의 인수가 rvalue 참조에 의해 전달되는 위치에 대한 오버로드 는 기존 버퍼에 연결하여 새 문자열을 모두 할당하는 것을 피할 수 있기 때문에 정확하지 않다고 생각합니다. 피연산자 중 하나입니다 (용량이 충분하지 않은 경우 재 할당해야 할 수도 있음).
underscore_d

2

작은 문자열의 경우 중요하지 않습니다. 큰 문자열이있는 경우 벡터 또는 일부 다른 컬렉션에있는 그대로 저장하는 것이 좋습니다. 그리고 하나의 큰 문자열 대신 이러한 데이터 세트로 작업하도록 알고리즘을 추가하십시오.

복잡한 연결을 위해 std :: ostringstream을 선호합니다.


2

대부분의 경우와 마찬가지로 무언가를하는 것보다하지 않는 것이 더 쉽습니다.

GUI에 큰 문자열을 출력하려는 ​​경우 출력하는 것이 무엇이든 큰 문자열보다 문자열을 조각으로 더 잘 처리 할 수 ​​있습니다 (예 : 텍스트 편집기에서 텍스트 연결-일반적으로 행을 별도의 줄로 유지) 구조).

파일로 출력하려면 큰 문자열을 만들어 출력하는 대신 데이터를 스트리밍하십시오.

느린 코드에서 불필요한 연결을 제거하면 연결을 더 빠르게 할 필요가 없습니다.


2

결과 문자열에 공간을 미리 할당 (예약)하면 성능이 가장 좋습니다.

template<typename... Args>
std::string concat(Args const&... args)
{
    size_t len = 0;
    for (auto s : {args...})  len += strlen(s);

    std::string result;
    result.reserve(len);    // <--- preallocate result
    for (auto s : {args...})  result += s;
    return result;
}

용법:

std::string merged = concat("This ", "is ", "a ", "test!");

0

배열 크기와 할당 된 바이트 수를 추적하는 클래스에 캡슐화 된 간단한 문자 배열이 가장 빠릅니다.

트릭은 처음에 하나의 큰 할당 만 수행하는 것입니다.

...에서

https://github.com/pedro-vicente/table-string

벤치 마크

Visual Studio 2015의 경우 x86 디버그 빌드, C ++ std :: string에 대한 실질적인 개선.

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  

1
OP는 어떻게 효율적으로 std::string. 그들은 대체 문자열 클래스를 요구하지 않습니다.
underscore_d

0

각 항목에 대한 메모리 예약으로 이것을 시도 할 수 있습니다.

namespace {
template<class C>
constexpr auto size(const C& c) -> decltype(c.size()) {
  return static_cast<std::size_t>(c.size());
}

constexpr std::size_t size(const char* string) {
  std::size_t size = 0;
  while (*(string + size) != '\0') {
    ++size;
  }
  return size;
}

template<class T, std::size_t N>
constexpr std::size_t size(const T (&)[N]) noexcept {
  return N;
}
}

template<typename... Args>
std::string concatStrings(Args&&... args) {
  auto s = (size(args) + ...);
  std::string result;
  result.reserve(s);
  return (result.append(std::forward<Args>(args)), ...);
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.