百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

Stackoverflow高赞问答如何一步步提升Go生成随机字符串的效率

toyiye 2024-06-21 12:00 9 浏览 0 评论

假如我们要生成一个固定长度的随机字符串,包含大小写字母,没有数字,没有特殊字符串,那么我们怎么做呢?需要怎样优化,才会更简单,更高效?在最终的方案之前,我们看看最常见的写法是怎样的,然后是如何一步步演进到最终的高效率方案的。好吧,先看下最原始的方案。

常见做法(Runes)

func init() {
    rand.Seed(time.Now().UnixNano())
}

var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

func RandStringRunes(n int) string {
    b := make([]rune, n)
    for i := range b {
        b[i] = letterRunes[rand.Intn(len(letterRunes))]
    }
    return string(b)
}

这个实现比较简单,二十六字母(大小写),然后随机取数,获得随机字符串。

Bytes改进

我们在最开始的时候进行了假设,我们的随机字符串只包含大小写字母,这样的话,我们发现没有必要使用rune类型存储,因为在Golang(Go语言)UTF-8编码下,英文字母和byte字节是一对一的。byte的本质是uint8类型,而rune本质是int32类型。我们改进后的代码如下:

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func RandStringBytes(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Intn(len(letterBytes))]
    }
    return string(b)
}

仔细看上面的代码,我们不光对rune类型进行了改进,还把原来的letter变量变成了常量,这样len(letterBytes)也是一个常量,代码的效率将大大提升。

余数改进

我们前面的方案都是通过调用rand.Intn()生成的随机字符,这个rand.Intn()其实是委托调用的Rand.Intn(),而Rand.Intn()最终又是调用的Rand.Int31n()实现。相比我们直接调用rand.Int63()来说,rand.Intn()要慢很多。

所以我们可以把rand.Intn()换成rand.Int63()来提高效率,为了不超过letterBytes的索引范围,我们使用余数来保证。

func RandStringBytesRmndr(n int) string {
    b := make([]byte, n)
    for i := range b {
        b[i] = letterBytes[rand.Int63() % int64(len(letterBytes))]
    }
    return string(b)
}

这种方式虽然快,但是有个缺点,就是每个字母的概率可能会不一样,不过52个字母相比1<<63-1是在太小太小,所以在这种情况下,这个缺点可以忽略不计。

Masking 掩码

基于前面的方案,我们可以进一步改进,使用随机数的最低位保证字母的均等分配,也就是掩码的方式。我们现在有52个字母,52用二进制表示就是52==110100b,所以我们可以只使用rand.Int63()返回最低的6位数就可以。为了保证平均分配,如果返回的只大于len(letterBytes)-1,则舍弃不用。

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
)

func RandStringBytesMask(n int) string {
    b := make([]byte, n)
    for i := 0; i < n; {
        if idx := int(rand.Int63() & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i++
        }
    }
    return string(b)
}

按照作者的推测,在52个字母的情况下,随机到超过范围的可能性(64-52)/64 = 0.19,按上面的代码,如果超过范围会重复生成,重复的10次的概率仅有1e-8。

Masking 掩码改进

上一步的方案,我们使用rand.Int63()可以生成63个随机位的数,但是我们只用了最低位的6个,有点浪费,因为获取随机数是我们整个代码中最慢的部分。现在我们有52个字母,意味着6位编码字母索引即可满足,所以我们使用rand.Int63()生成的随机数可以被我们使用63/6=10次。

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
const (
    letterIdxBits = 6                    // 6 bits to represent a letter index
    letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
    letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
)

func RandStringBytesMaskImpr(n int) string {
    b := make([]byte, n)
    // A rand.Int63() generates 63 random bits, enough for letterIdxMax letters!
    for i, cache, remain := n-1, rand.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = rand.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return string(b)
}

把生成的63位的随机数,分成10部分,每一部分都可以被我们使用,这样我们调用rand.Int63()次数将大大降低,进而提升效率。

rand Source 优化

rand.Rand其实是使用了一个rand.Source作为生成随机数的源,这个rand.Source是个接口,正好有个func Int63() int64 方法。

// A Source represents a source of uniformly-distributed
// pseudo-random int64 values in the range [0, 1<<63).
type Source interface {
    Int63() int64
    Seed(seed int64)
}

这正好是我们需要的,也够我们用了。改进后代码如下:

var src = rand.NewSource(time.Now().UnixNano())

func RandStringBytesMaskImprSrc(n int) string {
    b := make([]byte, n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return string(b)
}

原来的rand.Int63()是整个rand包全局的,而且支持安全高并发,所以速度比较慢。现在我们自己创建的这个src只有我们自己用,所以效率比较高。

strings.Builder 改进

这个是G0 1.10 新增的功能,提升字符串拼接的效率。

经过改进后,代码如下:

func RandStringBytesMaskImprSrcSB(n int) string {
    sb := strings.Builder{}
    sb.Grow(n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            sb.WriteByte(letterBytes[idx])
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return sb.String()
}

使用unsafe包模拟 strings.Builder

strings.Builder的原理其实很简单,是内置了一个[]byte存储字符,最终转换为string的时候为了避免拷贝,使用了unsafe包。

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

以上这些我们可以自己来做,看看我们重写后的代码。

func RandStringBytesMaskImprSrcUnsafe(n int) string {
    b := make([]byte, n)
    // A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }

    return *(*string)(unsafe.Pointer(&b))
}

效果和使用strings.Builder一样,而且看起来更简洁了。

Benchmark 性能测试

以后,我们通过一步步的改进代码,提升效率,现在我们通过Benchmark测试看下这些方法的效果。

BenchmarkRunes-4                     2000000    723 ns/op   96 B/op   2 allocs/op
BenchmarkBytes-4                     3000000    550 ns/op   32 B/op   2 allocs/op
BenchmarkBytesRmndr-4                3000000    438 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMask-4                 3000000    534 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMaskImpr-4            10000000    176 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMaskImprSrc-4         10000000    139 ns/op   32 B/op   2 allocs/op
BenchmarkBytesMaskImprSrcSB-4       10000000    134 ns/op   16 B/op   1 allocs/op
BenchmarkBytesMaskImprSrcUnsafe-4   10000000    115 ns/op   16 B/op   1 allocs/op

仅仅从rune到byte的改进,我们就获得了** 24%的提升,内存占用降低了三分之一** 。

使用rand.Int63替换掉原来的rand.Intn,我们又获得了近20%的提升。

单纯的使用掩码,因为重复获取可用索引的问题,性能下降了** -22%**。

但是当我们对 Masking 掩码 进行改进,分为10部分缓存的时候,我们获得了3倍的提升。

使用rand.Source 代替 rand.Rand, 我们再次获得了21%的提升。

使用strings.Builder,速度提升虽然只有3.5%,但是内存分配降低了50%

最后,通过unsafe包精简重写了strings.Builder的功能,我们又获得了14%的提升。

最终,RandStringBytesMaskImprSrcUnsafe比RandStringRunes快6.3倍,并且只使用了六分之一的内存和一半的内存分配,我们就完成了任务。

结语

这是一篇stackoverflow的文章,有人提问 How to generate a random string of a fixed length in Go? ,icza 做了非常精彩的回答,我把整个翻译下来加以整理分享给大家。

这是一篇非常棒的文章,它的意义不光是回答这个问题,还有帮助我们建立如何一步步优化的思路以及追求极致的极客精神。

与大家共勉。

原文地址:https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码