使用bytes.Buffer在Go中实现类似Fibonacci的字符串连接的正确方法是什么?

时间:2018-03-13 13:26:45

标签: go byte buffer string-concatenation

我在Go中使用“+”和bytes.Buffer(“WriteString”和“Write(bytes)”测试了简单的字符串连接。结果显示“+”比其他两个慢得多,这是有道理的。

然而,当我使用这三种方法来实现类似斐波那契的字符串连接(即a,b,ab,bab,abbab,bababbab,abbabbababbab)时,“+”表现最佳。样本代码和基准测试结果如下所示。

字符串“+”

func Fibonacci(n int) string {  
    FiboResult := ""
    prev_result := "a"
    next_result := "b"
    if n == 1{  
        FiboResult = "a"
    }else if n == 2 {  
        FiboResult = "b"
    }else{
        for i := 3; i <= n; i++ {
            FiboResult = prev_result + next_result
            prev_result = next_result
            next_result = FiboResult
        }
    }   
    return FiboResult
}

bytes.Buffer(WriteString)

func Fibonacci(n int) bytes.Buffer {  
    var FiboResult bytes.Buffer
    var prev_result bytes.Buffer
    prev_result.WriteString("a")
    var next_result bytes.Buffer
    next_result.WriteString("b")
    if n == 1{  
        FiboResult.WriteString("a")
    }else if n == 2 {  
        FiboResult.WriteString("b")
    }else{
        for i := 3; i <= n; i++ {
            FiboResult.Reset()
            FiboResult.WriteString(prev_result.String())
            FiboResult.WriteString(next_result.String())
            prev_result.Reset()
            prev_result.WriteString(next_result.String())
            next_result.Reset()
            next_result.WriteString(FiboResult.String())
        }
    }   
    return FiboResult
}

the benchmarking results

我认为bytes.Buffer.String()的开销是实现这一目标的。但在这种情况下,我无法弄清楚如何正确使用bytes.Buffer。或者我如何修改我的代码以避免问题?提示,示例代码或解释都很受欢迎。非常感谢提前!

2 个答案:

答案 0 :(得分:0)

在Go中,使用testing包进行基准测试。

编写合理有效的Go功能。不要执行不必要的转换。最小化分配和副本。等等。允许使用非ASCII字符,例如中文字符。允许包含多个字符的字符串。考虑使用字节切片。例如,

func fibonacciN(n int) uint64 {
    f := uint64(0)
    a, b := uint64(0), uint64(1)
    for i := 0; i < n; i++ {
        f, a, b = a, b, a+b
        if a > b {
            break
        }
    }
    return f
}

func Fibonacci(a, b string, n int) string {
    if n < 0 {
        n = 0
    }
    switch n {
    case 0:
        return ""
    case 1:
        return a
    case 2:
        return b
    }
    f := make([]byte, len(a)*int(fibonacciN(n-1))+len(b)*int(fibonacciN(n)))
    ab := a + b
    copy(f[len(f)-len(ab):], ab)
    for i := 4; i <= n; i++ {
        end := len(f) - (len(a)*int(fibonacciN(i-3)) + len(b)*int(fibonacciN(i-2)))
        start := len(f) - (len(a)*int(fibonacciN(i-1)) + len(b)*int(fibonacciN(i)))
        copy(f[start:end], f[end:])
    }
    return string(f)
}

基准功能。例如,n = 20,

$ go test fib_test.go -bench=. -benchmem
goos: linux
goarch: amd64
BenchmarkPeterSO-8    1000000     1851 ns/op    13568 B/op     2 allocs/op
BenchmarkPlus-8        500000     2493 ns/op    18832 B/op    18 allocs/op
BenchmarkBuffer-8      100000    12773 ns/op    90256 B/op    60 allocs/op
PASS
$ 

fib_test.go

package main

import (
    "bytes"
    "testing"
)

var benchN = 20

func fibonacciN(n int) uint64 {
    f := uint64(0)
    a, b := uint64(0), uint64(1)
    for i := 0; i < n; i++ {
        f, a, b = a, b, a+b
        if a > b {
            break
        }
    }
    return f
}

func FibonacciPeterSO(a, b string, n int) string {
    if n < 0 {
        n = 0
    }
    switch n {
    case 0:
        return ""
    case 1:
        return a
    case 2:
        return b
    }
    f := make([]byte, len(a)*int(fibonacciN(n-1))+len(b)*int(fibonacciN(n)))
    ab := a + b
    copy(f[len(f)-len(ab):], ab)
    for i := 4; i <= n; i++ {
        end := len(f) - (len(a)*int(fibonacciN(i-3)) + len(b)*int(fibonacciN(i-2)))
        start := len(f) - (len(a)*int(fibonacciN(i-1)) + len(b)*int(fibonacciN(i)))
        copy(f[start:end], f[end:])
    }
    return string(f)
}

func BenchmarkPeterSO(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibonacciPeterSO("a", "b", benchN)
    }
}

func FibonacciPlus(n int) string {
    FiboResult := ""
    prev_result := "a"
    next_result := "b"
    if n == 1 {
        FiboResult = "a"
    } else if n == 2 {
        FiboResult = "b"
    } else {
        for i := 3; i <= n; i++ {
            FiboResult = prev_result + next_result
            prev_result = next_result
            next_result = FiboResult
        }
    }
    return FiboResult
}

func BenchmarkPlus(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibonacciPlus(benchN)
    }
}

func FibonacciBuffer(n int) bytes.Buffer {
    var FiboResult bytes.Buffer
    var prev_result bytes.Buffer
    prev_result.WriteString("a")
    var next_result bytes.Buffer
    next_result.WriteString("b")
    if n == 1 {
        FiboResult.WriteString("a")
    } else if n == 2 {
        FiboResult.WriteString("b")
    } else {
        for i := 3; i <= n; i++ {
            FiboResult.Reset()
            FiboResult.WriteString(prev_result.String())
            FiboResult.WriteString(next_result.String())
            prev_result.Reset()
            prev_result.WriteString(next_result.String())
            next_result.Reset()
            next_result.WriteString(FiboResult.String())
        }
    }
    return FiboResult
}

func BenchmarkBuffer(b *testing.B) {
    for i := 0; i < b.N; i++ {
        FibonacciBuffer(benchN)
    }
}

var testN = benchN

func TestPeterSO(t *testing.T) {
    for n := 0; n <= testN; n++ {
        got := FibonacciPeterSO("a", "b", n)
        want := FibonacciPlus(n)
        if want != got {
            t.Errorf("want: %s got: %s", want, got)
        }
    }
}

答案 1 :(得分:0)

bytes.Buffer(或更快,更快strings.Builder)胜过简单+字符串连接,如果你想追加“很多”值,并在最后得到一次结果,因为与多次使用+相比,不需要中间分配。

你并没有以这种方式使用bytes.Buffer:你只需要在其中写一个string,然后获取其内容并重置它。这只是一次往返,结果是开销。

这里的问题是生成您正在寻找的Fibonacci字符串,这需要文本添加到缓冲区,而不是追加。并且bytes.Buffer仅支持附加,因此使用它不是一个很好的选择。

使用bytes.Buffer

生成反向

请注意,如果生成字符串的反转,则前置操作基本上是追加操作。这意味着如果我们首先生成结果的反转,我们可以使用bytes.Buffer执行追加,否则将需要前置。当然,附加的字符串也必须与前面的字符串相反。

当然,当我们完成时,我们必须扭转结果以获得我们原本想要的东西。

还要注意,当以迭代方式构建结果时,连续的中间结果是前一个和之前的结合。因此,为了获得第n个结果,我们可以简单地附加我们已经拥有的子串!这是一个很好的优化。

以下是它的样子:

func FibonacciReverseBuf(n int) string {
    switch n {
    case 0:
        return ""
    case 1:
        return "a"
    case 2:
        return "b"
    }

    prev, prev2 := 1, 1

    buf := bytes.NewBufferString("ba")

    for i := 3; i < n; i++ {
        buf.Write(buf.Bytes()[:buf.Len()-prev2])
        prev2, prev = prev, prev+prev2
    }

    // Reverse
    b := buf.Bytes()
    for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
        b[i], b[j] = b[j], b[i]
    }

    return string(b)
}

使用[]byteappend()

生成反向

另请注意,由于我们只是追加,我们可以轻松使用[]byte并使用内置append()函数:

func FibonacciReverse(n int) string {
    switch n {
    case 0:
        return ""
    case 1:
        return "a"
    case 2:
        return "b"
    }

    prev, prev2 := 1, 1

    b := []byte("ba")

    for i := 3; i < n; i++ {
        b = append(b, b[:len(b)-prev2]...)
        prev2, prev = prev, prev+prev2
    }

    // Reverse
    for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
        b[i], b[j] = b[j], b[i]
    }

    return string(b)
}

在单个copy()

中预分配和使用[]byte

但是,使用append()可能会导致重新分配,因为我们不知道缓冲区(结果)有多大。所以我们从一个小缓冲区开始,append()将根据需要增加它。此外,append()需要切片值(切片标头)分配。而且我们也必须扭转结果。

更快的解决方案是摆脱这些缺点。

首先让我们计算结果的大小(这主要是计算Fibonacci数),并在一步中分配所需的字节切片。

如果我们这样做,我们可以通过将部分缓冲区([]byte)复制到特定位置来执行“前置”操作。所以没有append(),没有重新分配,没有倒退。

这就是它的样子:

func Fibonacci(n int) string {
    switch n {
    case 0:
        return ""
    case 1:
        return "a"
    case 2:
        return "b"
    }

    fibs := make([]int, n)
    fibs[0], fibs[1] = 1, 1
    for i := 2; i < n; i++ {
        fibs[i] = fibs[i-1] + fibs[i-2]
    }

    l := fibs[n-1]
    b := make([]byte, l)
    b[l-2], b[l-1] = 'a', 'b'
    for i := 3; i < n; i++ {
        copy(b[l-fibs[i]:], b[l-fibs[i-2]:])
    }

    return string(b)
}

测试输出

为了测试上述函数是否给出了我们希望给出的结果,我们可以使用以下测试函数:

func TestFibonacci(t *testing.T) {
    cases := []struct {
        n   int
        exp string
    }{
        {0, ""},
        {1, "a"},
        {2, "b"},
        {3, "ab"},
        {4, "bab"},
        {5, "abbab"},
        {6, "bababbab"},
        {7, "abbabbababbab"},
    }

    funcs := []struct {
        name string
        f    func(int) string
    }{
        {"FibonacciReverseBuf", FibonacciReverseBuf},
        {"FibonacciReverse", FibonacciReverse},
        {"Fibonacci", Fibonacci},
    }

    for _, c := range cases {
        for _, f := range funcs {
            if got := f.f(c.n); got != c.exp {
                t.Errorf("%s: Expected: %s, got: %s, n: %d",
                    f.name, c.exp, got, c.n)
            }
        }
    }
}

基准测试结果

使用n = 20进行基准测试:

BenchmarkFibonacciReverseBuf-4   200000   10739 ns/op    18024 B/op   10 allocs/op
BenchmarkFibonacciReverse-4      100000   13208 ns/op    28864 B/op   10 allocs/op
BenchmarkFibonacci-4             500000    3383 ns/op    13728 B/op    3 allocs/op
BenchmarkPeterSO-4               300000    4417 ns/op    13568 B/op    2 allocs/op
BenchmarkPlus-4                  200000    6072 ns/op    18832 B/op   18 allocs/op
BenchmarkBuffer-4                 50000   29608 ns/op    90256 B/op   60 allocs/op

我们可以看到bytes.Buffer的使用比你的好多了。尽管如此,使用连接更快,因为这里没有很多连接,它们是小连接,并且最终不需要反转。

另一方面,我的Fibonacci()解决方案优于所有其他解决方案。