Go:创建高性能的字符串

汪明军_3145 · · 546 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

【译文】原文地址
我最近一直在用空闲时间来解决LeetCode中的问题,我认为这是一个锻炼Golang的机会,并使用Go作为练习LeetCode的主要语言。Go是一种令人惊叹的编程语言,我从根本上赞同go语言的设计,即一种和C语言一样高性能。而且和JavaScript和Python这些脚本语言一样高效和易于编程。我建议你有机会也学习和尝试下。是的,我非常喜欢Go。

下面回到我LeetCode问题:leetcode 1249,该问题要求删除最少数量的圆括号,直到字符串达到平衡,即字母加上有效圆括号。首先,该问题解决方法需要构建一组需要删除圆括号的索引,然后循环遍历字符串的每个字符,并构建一个新的包含所需的有效字符的字符串(减去无效的圆括号)。

因此我解决了这个问题,但是总觉得有些不对劲。我的运行时间是824ms,击败了大约25.29%的Go提交者。我在想Go不是应该很快的吗?这也是我学习这门语言的原因。我知道我的方案已经优化了的,因为我验证了作者的解决方法,因此应该不是算法的原因,尽管有一点也不是最根本原因。



后来我用JavaScript重写了这个问题,看看运行效果如何。令我惊讶的是,在这个问题上JavaScript比Golang运行速度快了9倍。等等,什么?感觉不对。这时我开始进一步调查原因。我再次运行了这个问题的程序看是否是编译错误或其他原因,但不是Golang还是比JavaScript慢9倍。
Golang代码:

package main

import (
   "fmt"
)

func minRemoveToMakeValid(s string) string {

   stack := make([]int, 0)
   toRemove := make([]int, 0)

   for i := 0; i < len(s); i++ {

      if string(s[i]) == "(" {
         stack = append(stack, i)
      }else if string(s[i]) == ")" && len(stack) == 0 {
         toRemove = append(toRemove, i)
      } else if string(s[i]) == ")" && len(stack) > 0 {
         stack = stack[:len(stack) - 1]
      }
   }

   newString := ""
   toRemove = append(toRemove, stack...)

   removeMap := make(map[int]bool)

   for i:=0; i<len(toRemove); i++ {
      removeMap[toRemove[i]] = true
   }

   for i := 0; i < len(s); i++ {
      if !removeMap[i] {
         newString += string(s[i])
      }
   }

   return newString
}

func main() {
   s:= minRemoveToMakeValid("lee(t(c)o)de)")
   fmt.Println(s)
}

JavaScript代码:

/**
 * @param {string} s
 * @return {string}
 */
const minRemoveToMakeValid = (s) => {
    let stack = [];
    let toRemove = [];
    let stringOut = ""

    for(let i=0; i< s.length;i++){
        if(s[i] === "("){
            stack.push(i)
        }else if(s[i] === ")" && stack.length === 0){
            toRemove.push(i)

        }else if(s[i] === ")" && stack.length > 0){
            stack.pop();
        }
    }

    toRemove = toRemove.concat(stack)
    let removeMap = {}

    for(let i = 0; i<toRemove.length; i++){
        removeMap[toRemove[i]] = true
    }

    for(let i=0; i< s.length;i++){
        if(!removeMap[i]){
            stringOut+=s[i]
        }
    }

    return stringOut
};

console.log(minRemoveToMakeValid("lee(t(c)o)de)"));

正如你所看到的,以上两种代码几乎是相同的,所以这并不是js代码有特殊的优化。JS只是重写了go代码,两种方法应该是可比较的,而不是快这么多。

我在网上对这个问题进一步研究,我发现Golang字符串string对象是不可变的,运行时对字符串截取部分或者增加新的字符到字符串常量,Go都会创建新的string对象并拷贝。

让我们看看那些非常低效的字符串代码:

newString := ""
newString += string(s[i])
for(let i=0; i< s.length;i++){
    if(!removeMap[i]){
        stringOut+=s[i]
    }
}

在JavaScript中,字符串常量的创建是非常常用的,因此初始化空字符串并添加字符到字符串是有优化过的。但是Golang字符串是不可变的,所以每次添加字符到字符串变量,Go都会创建一个字符串的拷贝,这从内存和运行的角度来看都是很低效的。

在Golang中,如果你想创建一个字符串,需要使用strings包。Builder类型是bytes.Buffer的一个子集。Golang将字符串视为字节切片,其中每个字节都是字符串中字符的ASCII码。如果您想在内存和运行时上高效地对字符串进行操作,就需要使用byte buffer或者更高级的字符串结构体。
因此,我继续优化之前的Go代码:

// --- Initialize the string builder
var b strings.Builder
b.Grow(len(s) - len(toRemove))
// --- Write bytes in the bytes buffer
for i := 0; i < len(s); i++ {
   if !removeMap[i] {
      b.Write([]byte{s[i]})
   }
}

在我解决了这个优化的问题后,我再次提交我的解决方案,令我惊讶的是:



通过这个小小的优化,我可以使我的Golang程序运行速度提高80倍。同样,我的程序比最初的版本快80被,比JavaScript版本的快9倍。这下验证了我当初的信念Go确实是一门快速的编程语言。
因此,对刚刚接触Golang的程序员来说,清楚这类细节,因为如果不正确使用Go,您的生产应用程序可能运行的很慢,会失去许多优化和语言优势。


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:汪明军_3145

查看原文:Go:创建高性能的字符串

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

546 次点击  
加入收藏 微博
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传