Program Tip

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

programtip 2020. 9. 30. 11:20
반응형

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


Go에서 a string는 원시 유형입니다. 즉, 읽기 전용이며 모든 조작이 새 문자열을 생성합니다.

따라서 결과 문자열의 길이를 모르고 문자열을 여러 번 연결하려는 경우 가장 좋은 방법은 무엇입니까?

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

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

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


2018 년에 추가 된 메모

Go 1.10에는 strings.Builder유형 있습니다. 자세한 내용은이 답변을 참조하십시오 .

20x 이전 답변

가장 좋은 방법은 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입니다. 내 테스트에서 그 접근 방식은 사용하는 것보다 ~ 3 배 빠르며 bytes.Bufferoperator를 사용하는 것보다 훨씬 빠릅니다 (~ 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, 여기 .

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


용법:

.NET과 거의 동일합니다 bytes.Buffer.

package main

import (
    "strings"
    "fmt"
)

func main() {
    var str strings.Builder

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

    fmt.Println(str.String())
}

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


지원하는 StringBuilder 메서드 및 인터페이스 :

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


0 값 사용 :

var buf strings.Builder

bytes.Buffer와의 차이점 :

  • 성장하거나 재설정 할 수만 있습니다.

  • 에서는 다음 bytes.Buffer과 같이 기본 바이트에 액세스 할 수 있습니다. (*Buffer).Bytes(); strings.Builder이 문제를 방지합니다. 때로는 이것은 문제가되지 않고 대신 원하는 경우가 있습니다 (예 : 바이트가 기타로 전달 될 때 엿보기 동작 io.Reader).

  • 또한 실수로 복사하는 것을 방지하는 copyCheck 메커니즘이 내장되어 있습니다 ( func (b *Builder) copyCheck() { ... }).


여기에서 소스 코드를 확인 하십시오 .


문자열 패키지에는 http://golang.org/pkg/strings/#Join 이라는 라이브러리 함수가 있습니다 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)

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


package main

import (
  "fmt"
)

func main() {
    var str1 = "string1"
    var str2 = "string2"
    out := fmt.Sprintf("%s %s ",str1, str2)
    fmt.Println(out)
}

2018 년에 추가 된 메모

Go 1.10에는 strings.Builder유형 있습니다. 자세한 내용은이 답변을 참조하십시오 .

20x 이전 답변

@ cd1 및 기타 답변의 벤치 마크 코드가 잘못되었습니다. b.N벤치 마크 기능에서 설정하면 안됩니다. 테스트 실행 시간이 안정적인지 확인하기 위해 go 테스트 도구에 의해 동적으로 설정됩니다.

벤치 마크 함수는 동일한 테스트 b.N시간을 실행 해야하며 루프 내부의 테스트는 각 반복에 대해 동일해야합니다. 그래서 내부 루프를 추가하여 수정합니다. 다른 솔루션에 대한 벤치 마크도 추가합니다.

package main

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

const (
    sss = "xfoasneobfasieongasbg"
    cnt = 10000
)

var (
    bbb      = []byte(sss)
    expected = strings.Repeat(sss, cnt)
)

func BenchmarkCopyPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        bs := make([]byte, cnt*len(sss))
        bl := 0
        for i := 0; i < cnt; i++ {
            bl += copy(bs[bl:], sss)
        }
        result = string(bs)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkAppendPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, cnt*len(sss))
        for i := 0; i < cnt; i++ {
            data = append(data, sss...)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferPreAllocate(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        buf := bytes.NewBuffer(make([]byte, 0, cnt*len(sss)))
        for i := 0; i < cnt; i++ {
            buf.WriteString(sss)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkCopy(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, 64) // same size as bootstrap array of bytes.Buffer
        for i := 0; i < cnt; i++ {
            off := len(data)
            if off+len(sss) > cap(data) {
                temp := make([]byte, 2*cap(data)+len(sss))
                copy(temp, data)
                data = temp
            }
            data = data[0 : off+len(sss)]
            copy(data[off:], sss)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkAppend(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        data := make([]byte, 0, 64)
        for i := 0; i < cnt; i++ {
            data = append(data, sss...)
        }
        result = string(data)
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferWrite(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var buf bytes.Buffer
        for i := 0; i < cnt; i++ {
            buf.Write(bbb)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkBufferWriteString(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var buf bytes.Buffer
        for i := 0; i < cnt; i++ {
            buf.WriteString(sss)
        }
        result = buf.String()
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

func BenchmarkConcat(b *testing.B) {
    var result string
    for n := 0; n < b.N; n++ {
        var str string
        for i := 0; i < cnt; i++ {
            str += sss
        }
        result = str
    }
    b.StopTimer()
    if result != expected {
        b.Errorf("unexpected result; got=%s, want=%s", string(result), expected)
    }
}

환경은 OS X 10.11.6, 2.2GHz Intel Core i7입니다.

시험 결과:

BenchmarkCopyPreAllocate-8         20000             84208 ns/op          425984 B/op          2 allocs/op
BenchmarkAppendPreAllocate-8       10000            102859 ns/op          425984 B/op          2 allocs/op
BenchmarkBufferPreAllocate-8       10000            166407 ns/op          426096 B/op          3 allocs/op
BenchmarkCopy-8                    10000            160923 ns/op          933152 B/op         13 allocs/op
BenchmarkAppend-8                  10000            175508 ns/op         1332096 B/op         24 allocs/op
BenchmarkBufferWrite-8             10000            239886 ns/op          933266 B/op         14 allocs/op
BenchmarkBufferWriteString-8       10000            236432 ns/op          933266 B/op         14 allocs/op
BenchmarkConcat-8                     10         105603419 ns/op        1086685168 B/op    10000 allocs/op

결론:

  1. CopyPreAllocate가장 빠른 방법입니다. AppendPreAllocateNo.1에 매우 가깝지만 코드를 작성하는 것이 더 쉽습니다.
  2. Concat속도와 메모리 사용량 모두에서 성능이 정말 나쁩니다. 그것을 사용하지 마십시오.
  3. Buffer#Write그리고 Buffer#WriteString기본적으로 다니 - 브롬은 코멘트에서 밝혔다 @ 것과는 달리, 속도가 동일합니다. string실제로 []byteGo에 있음을 고려 하면 의미가 있습니다.
  4. bytes.Buffer는 기본적으로 Copy여분의 책 보관 및 기타 항목과 동일한 솔루션을 사용합니다 .
  5. Copy그리고 Appendbytes.Buffer와 같은 64의 부트 스트랩 크기를 사용합니다.
  6. Append더 많은 메모리와 할당을 사용하면 사용하는 성장 알고리즘과 관련이 있다고 생각합니다. 메모리가 바이트만큼 빠르게 증가하지 않습니다.

암시:

  1. OP가 원하는 것과 같은 간단한 작업의 경우 Append또는 AppendPreAllocate. 충분히 빠르고 사용하기 쉽습니다.
  2. 동시에 버퍼를 읽고 쓸 필요가 있다면 bytes.Buffer물론 사용하십시오 . 그것이 바로 그것이 설계된 것입니다.

내 원래 제안은

s12 := fmt.Sprint(s1,s2)

그러나 bytes.Buffer-WriteString ()을 사용하는 위의 대답 이 가장 효율적인 방법입니다.

내 초기 제안은 반사와 유형 스위치를 사용합니다. 참조 (p *pp) doPrint(p *pp) printArg
내가 순진하게 생각했던대로 보편적 인 스트링거 () 인터페이스는 기본 유형이 없습니다.

적어도 Sprint ()는 내부적 으로 bytes.Buffer를 사용합니다. 그러므로

`s12 := fmt.Sprint(s1,s2,s3,s4,...,s1000)`

메모리 할당 측면에서 허용됩니다.

=> Sprint () 연결은 빠른 디버그 출력을 위해 사용할 수 있습니다.
=> 그렇지 않으면 bytes.Buffer를 사용하십시오.


cd1의 대답으로 확장 : copy () 대신 append ()를 사용할 수 있습니다. append ()는 더 큰 사전 프로비저닝을 수행하여 약간의 메모리 비용이 들지만 시간을 절약합니다. 나는 당신의 상단에 두 개의 벤치 마크 추가했습니다 . 로컬에서 실행

go test -bench=. -benchtime=100ms

내 씽크 패드 T400에서는 다음과 같은 결과가 나옵니다.

BenchmarkAppendEmpty    50000000         5.0 ns/op
BenchmarkAppendPrealloc 50000000         3.5 ns/op
BenchmarkCopy           20000000        10.2 ns/op

이것은 @icza 및 @PickBoy에서 언급 한 버그 수정과 함께 @ cd1 ( Go 1.8, linux x86_64)에서 제공 한 실제 벤치 마크 버전입니다 .

Bytes.Buffer아니라 7시간이 빨리를 통해 직접 문자열 연결에 비해 +연산자.

package performance_test

import (
    "bytes"
    "fmt"
    "testing"
)

const (
    concatSteps = 100
)

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

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

타이밍 :

BenchmarkConcat-4                             300000          6869 ns/op
BenchmarkBuffer-4                            1000000          1186 ns/op

goutils.JoinBetween

 func JoinBetween(in []string, separator string, startIndex, endIndex int) string {
    if in == nil {
        return ""
    }

    noOfItems := endIndex - startIndex

    if noOfItems <= 0 {
        return EMPTY
    }

    var builder strings.Builder

    for i := startIndex; i < endIndex; i++ {
        if i > startIndex {
            builder.WriteString(separator)
        }
        builder.WriteString(in[i])
    }
    return builder.String()
}

다음을 사용하여 수행합니다.

package main

import (
    "fmt"
    "strings"
)

func main (){
    concatenation:= strings.Join([]string{"a","b","c"},"") //where second parameter is a separator. 
    fmt.Println(concatenation) //abc
}

package main

import (
"fmt"
)

func main() {
    var str1 = "string1"
    var str2 = "string2"
    result := make([]byte, 0)
    result = append(result, []byte(str1)...)
    result = append(result, []byte(str2)...)
    result = append(result, []byte(str1)...)
    result = append(result, []byte(str2)...)

    fmt.Println(string(result))
}

메모리 할당 통계를 사용한 벤치 마크 결과. github 에서 벤치 마크 코드를 확인하십시오 .

strings.Builder를 사용하여 성능을 최적화하십시오.

go test -bench . -benchmem
goos: darwin
goarch: amd64
pkg: github.com/hechen0/goexp/exps
BenchmarkConcat-8                1000000             60213 ns/op          503992 B/op          1 allocs/op
BenchmarkBuffer-8               100000000               11.3 ns/op             2 B/op          0 allocs/op
BenchmarkCopy-8                 300000000                4.76 ns/op            0 B/op          0 allocs/op
BenchmarkStringBuilder-8        1000000000               4.14 ns/op            6 B/op          0 allocs/op
PASS
ok      github.com/hechen0/goexp/exps   70.071s

s := fmt.Sprintf("%s%s", []byte(s1), []byte(s2))

strings.Join() "문자열"패키지에서

유형 불일치가있는 경우 (예 : int 및 문자열을 결합하려는 경우), RANDOMTYPE (변경하려는 것)을 수행합니다.

전의:

package main

import (
    "fmt"
    "strings"
)

var intEX = 0
var stringEX = "hello all you "
var stringEX2 = "people in here"


func main() {
    s := []string{stringEX, stringEX2}
    fmt.Println(strings.Join(s, ""))
}

출력 :

hello all you people in here

참고 URL : https://stackoverflow.com/questions/1760757/how-to-efficiently-concatenate-strings

반응형