[string] 효율적으로 문자열을 연결하는 방법

Go에서 a string는 기본 유형이므로 읽기 전용이므로 모든 조작시 새 문자열이 작성됩니다.

결과 문자열의 길이를 모른 채 문자열을 여러 번 연결하려면 가장 좋은 방법은 무엇입니까?

순진한 방법은 다음과 같습니다.

s := ""
for i := 0; i < 1000; i++ {
    s += getShortStringFromSomewhere()
}
return s

그러나 그것은 매우 효율적으로 보이지 않습니다.



답변

새로운 길:

Go 1.10부터 strings.Builder유형 이 있으므로 자세한 내용은이 답변을 참조하십시오 .

올드 웨이 :

bytes패키지를 사용하십시오 . Buffer구현 하는 유형이 있습니다 io.Writer.

package main

import (
    "bytes"
    "fmt"
)

func main() {
    var buffer bytes.Buffer

    for i := 0; i < 1000; i++ {
        buffer.WriteString("a")
    }

    fmt.Println(buffer.String())
}

이것은 O (n) 시간 안에 이루어집니다.


답변

문자열을 연결하는 가장 효율적인 방법은 내장 함수를 사용하는 것 copy입니다. 내 테스트에서 그 접근 방식은 bytes.Buffer연산자를 사용하는 것보다 ~ 3 배 빠르며 연산자를 사용하는 것보다 훨씬 빠릅니다 (~ 12,000x) +. 또한 적은 메모리를 사용합니다.

이를 증명하기 위해 테스트 사례 를 만들었 으며 결과는 다음과 같습니다.

BenchmarkConcat  1000000    64497 ns/op   502018 B/op   0 allocs/op
BenchmarkBuffer  100000000  15.5  ns/op   2 B/op        0 allocs/op
BenchmarkCopy    500000000  5.39  ns/op   0 B/op        0 allocs/op

테스트 코드는 다음과 같습니다.

package main

import (
    "bytes"
    "strings"
    "testing"
)

func BenchmarkConcat(b *testing.B) {
    var str string
    for n := 0; n < b.N; n++ {
        str += "x"
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); str != s {
        b.Errorf("unexpected result; got=%s, want=%s", str, s)
    }
}

func BenchmarkBuffer(b *testing.B) {
    var buffer bytes.Buffer
    for n := 0; n < b.N; n++ {
        buffer.WriteString("x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); buffer.String() != s {
        b.Errorf("unexpected result; got=%s, want=%s", buffer.String(), s)
    }
}

func BenchmarkCopy(b *testing.B) {
    bs := make([]byte, b.N)
    bl := 0

    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        bl += copy(bs[bl:], "x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); string(bs) != s {
        b.Errorf("unexpected result; got=%s, want=%s", string(bs), s)
    }
}

// Go 1.10
func BenchmarkStringBuilder(b *testing.B) {
    var strBuilder strings.Builder

    b.ResetTimer()
    for n := 0; n < b.N; n++ {
        strBuilder.WriteString("x")
    }
    b.StopTimer()

    if s := strings.Repeat("x", b.N); strBuilder.String() != s {
        b.Errorf("unexpected result; got=%s, want=%s", strBuilder.String(), s)
    }
}


답변

Go 1.10+에는 strings.Builder, here이 있습니다 .

Builder는 Write 메서드를 사용하여 문자열을 효율적으로 작성하는 데 사용됩니다. 메모리 복사를 최소화합니다. 0 값을 사용할 준비가되었습니다.


와 거의 동일합니다 bytes.Buffer.

package main

import (
    "strings"
    "fmt"
)

func main() {
    // ZERO-VALUE:
    //
    // It's ready to use from the get-go.
    // You don't need to initialize it.
    var str strings.Builder

    for i := 0; i < 1000; i++ {
        str.WriteString("a")
    }

    fmt.Println(str.String())
}

놀이터에서 이것을 보려면 클릭하십시오 .


노트

  • 기본 데이터를 캐시하므로 StringBuilder 값을 복사하지 마십시오.
  • StringBuilder 값을 공유하려면 포인터를 사용하십시오.

지원되는 인터페이스

StringBuilder의 메소드는 기존 인터페이스를 염두에두고 구현되고 있습니다. 코드에서 쉽게 새 빌더 유형으로 전환 할 수 있습니다.


바이트와의 차이점

  • 커지거나 재설정 만 할 수 있습니다.

  • 여기에는 실수로 복사하는 것을 방지하는 copyCheck 메커니즘이 내장되어 있습니다.

    func (b *Builder) copyCheck() { ... }

  • 에서 다음 bytes.Buffer과 같이 기본 바이트에 액세스 할 수 있습니다 (*Buffer).Bytes().

    • strings.Builder 이 문제를 방지합니다.
    • 때때로, 이것은 문제가되지 않고 대신에 바람직합니다.
    • 예를 들면 : 바이트 등을 전달할 때의 엿보기 동작의 경우 io.Reader.

자세한 내용은 소스 코드를 확인하십시오 ( 여기) .


답변

문자열 패키지에는 다음과 같은 라이브러리 함수가 있습니다 Join.
http://golang.org/pkg/strings/#Join

코드를 Join살펴보면 Kinopiko가 작성한 Append 함수에 대한 비슷한 접근 방식이 표시됩니다. https://golang.org/src/strings/strings.go#L420

용법:

import (
    "fmt";
    "strings";
)

func main() {
    s := []string{"this", "is", "a", "joined", "string\n"};
    fmt.Printf(strings.Join(s, " "));
}

$ ./test.bin
this is a joined string


답변

방금 내 코드 (재귀 트리 워크)에서 위에 게시 된 최상위 답변을 벤치마킹했으며 간단한 concat 연산자가 실제로보다 빠릅니다 BufferString.

func (r *record) String() string {
    buffer := bytes.NewBufferString("");
    fmt.Fprint(buffer,"(",r.name,"[")
    for i := 0; i < len(r.subs); i++ {
        fmt.Fprint(buffer,"\t",r.subs[i])
    }
    fmt.Fprint(buffer,"]",r.size,")\n")
    return buffer.String()
}

다음 코드는 0.81 초가 걸렸습니다.

func (r *record) String() string {
    s := "(\"" + r.name + "\" ["
    for i := 0; i < len(r.subs); i++ {
        s += r.subs[i].String()
    }
    s += "] " + strconv.FormatInt(r.size,10) + ")\n"
    return s
} 

0.61 초 밖에 걸리지 않았습니다. 이것은 아마도 새로운 생성의 오버 헤드 때문일 것입니다 BufferString.

업데이트 : 또한 join기능을 벤치마킹 했으며 0.54 초 만에 실행되었습니다.

func (r *record) String() string {
    var parts []string
    parts = append(parts, "(\"", r.name, "\" [" )
    for i := 0; i < len(r.subs); i++ {
        parts = append(parts, r.subs[i].String())
    }
    parts = append(parts, strconv.FormatInt(r.size,10), ")\n")
    return strings.Join(parts,"")
}


답변

큰 바이트 조각을 만들고 문자열 조각을 사용하여 짧은 문자열의 바이트를 복사 할 수 있습니다. “Effective Go”에는 다음과 같은 기능이 있습니다.

func Append(slice, data[]byte) []byte {
    l := len(slice);
    if l + len(data) > cap(slice) { // reallocate
        // Allocate double what's needed, for future growth.
        newSlice := make([]byte, (l+len(data))*2);
        // Copy data (could use bytes.Copy()).
        for i, c := range slice {
            newSlice[i] = c
        }
        slice = newSlice;
    }
    slice = slice[0:l+len(data)];
    for i, c := range data {
        slice[l+i] = c
    }
    return slice;
}

그런 다음 작업이 완료되면 string ( )큰 바이트 조각을 사용 하여 문자열로 다시 변환하십시오.


답변

전체 버퍼 크기를 먼저 알거나 계산할 필요가없는 가장 빠른 솔루션입니다.

var data []byte
for i := 0; i < 1000; i++ {
    data = append(data, getShortStringFromSomewhere()...)
}
return string(data)

벤치 마크 에 따르면 복사 솔루션보다 20 % 느리지 만 (6.72ns가 아닌 추가 당 8.1ns) bytes.Buffer를 사용하는 것보다 여전히 55 % 빠릅니다.