快速提升 Go 程序性能的实用技巧,你值得了解一下。

作者 | Stephen Whitworth

译者 | 弯月,责编 | 屠敏

出品 | CSDN(ID:CSDNnews)

我对软件性能的话题十分感兴趣。虽然我说不清究竟是为什么。我忍受不了慢吞吞的服务和程序,而且似乎有此种感受的人不止我一个,比如还有Greg Linden:

我们尝试过在A/B测试中,将页面的延迟增加100毫秒,结果发现如此微小的延迟也会导致整体性能的大幅下降。

——亚马逊,Greg Linden

根据我的经验,糟糕的性能通常来自两个方面:

  • 小规模时性能还不错,但用户数增长后就无法使用的操作。这类操作通常是O(N)或O(N²)。当用户基数小时,它们的性能还可以接受,所以经常在将产品推向市场阶段使用。随着用户基数的增长,这类操作会出现越来越多意想不到的异常,服务也变得不稳定。

  • 出于不同人之手的大量小型优化——即“千疮百孔”的复杂代码。

我的职业生涯主要做两项工作,第一是用Python写数据科学脚本,第二是用Go写服务。关于后者我有更多的优化经验。Go通常不会是服务的瓶颈,因为这些程序通常需要访问数据库,因此更偏重于IO。相反,批处理的机器学习管线(前者)的程序通常偏重于CPU。如果Go语言使用了过多CPU,而且造成了负面影响,那么你可以采用几种策略来应对。

我在这篇文章中列出了一些不需要太多精力就能显著提高性能的技巧,并不包含那些需要太多精力或需要大幅度修改程序结构的技巧。


开始优化之前

开始优化之前,首先应该花些时间找出一个合适的基准线,以便稍后比较。如果没有基准,那就等于摸着石头过河,根本不知道自己的优化有没有效果。首先要编写性能测试程序,然后生成能用于pprof的profile文件。最好可以编写Go的性能测试脚本(https://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go),这样可以很容易地使用pprof,还可以评测内存分配情况。还可以使用benchcmp,这个工具可以帮助比较两次性能测试之间的性能差异。

如果代码很难做性能测试,那就从你能测量时间的部分开始。可以利用runtime/pprof手工测量代码。

现在开始吧!

使用sync.Pool重用之前分配过的对象

sync.Pool实现了一个空闲列表(free-list)。这样可以重新使用之前分配过的对象。这样做可以将对象分配的代价平摊到多次使用上,减少垃圾回收器的工作。API非常简单:只需实现一个函数,用来分配新的对象即可。它会返回指针类型。

var bufpool = sync.Pool{    New: func() interface{} {        buf := make([]byte, 512)        return &buf    }}    New: func() interface{} {        buf := make([]byte, 512)        return &buf    }}

之后,可以用Get()从池中获取对象,用完之后用Put()将对象放回。

// sync.Pool returns a interface{}: you must cast it to the underlying type// before you use it.bp := bufpool.Get().(*[]byte)b := *bpdefer func() {    *bp = b    bufpool.Put(bp)}()// Now, go do interesting things with your byte buffer.buf := bytes.NewBuffer(b)// before you use it.bp := bufpool.Get().(*[]byte)b := *bpdefer func() {    *bp = b    bufpool.Put(bp)}()

// Now, go do interesting things with your byte buffer.buf := bytes.NewBuffer(b)

不过要注意一些陷阱。在Go 1.13之前,每次发生垃圾回收时该池都会被清空。对于需要分配大量对象的程序来说,这可能会造成性能的影响。在1.13版本中似乎GC后能保留更多对象了(https://go-review.googlesource.com/c/go/+/162919/)。

你可能需要在将对象放回池中之前将其结构的字段清空。

如果不这样做,就可能从池中获得一个“脏”的对象,它包含之前使用过的数据。这可能会造成严重的安全问题!

type AuthenticationResponse {    Token string    UserID string}rsp := authPool.Get().(*AuthenticationResponse)defer authPool.Put(rsp)// If we don't hit this if statement, we might return data from other users! ?if blah {    rsp.UserID = "user-1"    rsp.Token = "super-secret}return rsp    UserID string}

rsp := authPool.Get().(*AuthenticationResponse)defer authPool.Put(rsp)

// If we don't hit this if statement, we might return data from other users! ?if blah {    rsp.UserID = "user-1"    rsp.Token = "super-secret}

return rsp

安全的做法就是明确清空内存:

// reset resets all fields of the AuthenticationResponse before pooling it.func (a* AuthenticationResponse) reset() {    a.Token = ""    a.UserID = ""}rsp := authPool.Get().(*AuthenticationResponse)defer func() {    rsp.reset()    authPool.Put(rsp)}()func (a* AuthenticationResponse) reset() {    a.Token = ""    a.UserID = ""}

rsp := authPool.Get().(*AuthenticationResponse)defer func() {    rsp.reset()    authPool.Put(rsp)}()

唯一不会发生问题的情况就是读取和写入时使用的内存是同一片的情况。例如:

var (    r io.Reader    w io.Writer)// Obtain a buffer from the pool.buf := *bufPool.Get().(*[]byte)defer bufPool.Put(&buf)// We only write to w exactly what we read from r, and no more. ?nr, er := r.Read(buf)if nr > 0 {    nw, ew := w.Write(buf[0:nr])}    r io.Reader    w io.Writer)

// Obtain a buffer from the pool.buf := *bufPool.Get().(*[]byte)defer bufPool.Put(&buf)

// We only write to w exactly what we read from r, and no more. ?nr, er := r.Read(buf)if nr > 0 {    nw, ew := w.Write(buf[0:nr])}

在大map中避免使用包含指针的结构作为map的键

关于Go中大型堆的性能问题已经有很多人讨论过了。在垃圾回收过程中,运行时会扫描包含指针的对象并遍历其指针。如果你有非常大的map[string]int,那么垃圾回收器就不得不在每次垃圾回收过程中检查map中的每个字符串,因为字符串包含指针。

这个例子中我们向一个map[string]int中写入了一千万个元素,然后测量垃圾回收的时间。map是在包的作用域中分配的,以保证它被分配到堆上。

package mainimport (    "fmt"    "runtime"    "strconv"    "time")const (    numElements = 10000000)var foo = map[string]int{}func timeGC() {    t := time.Now()    runtime.GC()    fmt.Printf("gc took: %s\n", time.Since(t))}func main() {    for i := 0; i < numElements; i++ {        foo[strconv.Itoa(i)] = i    }    for {        timeGC()        time.Sleep(1 * time.Second)    }}

import (    "fmt"    "runtime"    "strconv"    "time")

const (    numElements = 10000000)

var foo = map[string]int{}

func timeGC() {    t := time.Now()    runtime.GC()    fmt.Printf("gc took: %s\n", time.Since(t))}

func main() {    for i := 0; i < numElements; i++ {        foo[strconv.Itoa(i)] = i    }

    for {        timeGC()        time.Sleep(1 * time.Second)    }}

运行后可以得到以下结果:

? inthash → go install && inthashgc took: 98.726321msgc took: 105.524633msgc took: 102.829451msgc took: 102.71908msgc took: 103.084104msgc took: 104.821989msgo install && inthashgc took: 98.726321msgc took: 105.524633msgc took: 102.829451msgc took: 102.71908msgc took: 103.084104msgc took: 104.821989ms

对于计算机来说这花得时间太多了!

怎样可以改进呢?最好是能尽量去掉指针,这样能减少垃圾回收器需要遍历的指针数量。由于字符串包含指针,因此我们可以用map[int]int来实现:

package mainimport (    "fmt"    "runtime"    "time")const (    numElements = 10000000)var foo = map[int]int{}func timeGC() {    t := time.Now()    runtime.GC()    fmt.Printf("gc took: %s\n", time.Since(t))}func main() {    for i := 0; i < numElements; i++ {        foo[i] = i    }    for {        timeGC()        time.Sleep(1 * time.Second)    }}

import (    "fmt"    "runtime"    "time")

const (    numElements = 10000000)

var foo = map[int]int{}

func timeGC() {    t := time.Now()    runtime.GC()    fmt.Printf("gc took: %s\n", time.Since(t))}

func main() {    for i := 0; i < numElements; i++ {        foo[i] = i    }

    for {        timeGC()        time.Sleep(1 * time.Second)    }}

重新运行程序,结果如下:

? inthash → go install && inthashgc took: 3.608993msgc took: 3.926913msgc took: 3.955706msgc took: 4.063795msgc took: 3.91519msgc took: 3.75226msgo install && inthashgc took: 3.608993msgc took: 3.926913msgc took: 3.955706msgc took: 4.063795msgc took: 3.91519msgc took: 3.75226ms

好多了。垃圾回收的时间减少了97%。在生产环境下,字符串需要进行hash之后再插入到map中。

还有许多技巧可以避免垃圾回收。如果为一个不含指针的结构(如int或byte)分配了巨大的数组,那么垃圾回收器就不会扫描它,意味着没有任何垃圾回收的额外开销。这种技巧通常需要重写大量程序,所以这里就不再细谈了。

与任何优化一样,该技巧的效果因人而异。Damian Gryski的一系列推特(https://twitter.com/dgryski/status/1140685755578118144)介绍了一个有趣的例子,从一个大型map中去掉字符串而使用智能数据结构,实际上会增加内存开销。我建议你阅读下他的文章。

生成marshalling代码以避免运行时反射

将数据结构marshalh或unmarshal成JSON等各种序列化格式是个很常见的操作,特别是在构建微服务的时候。实际上,大部分微服务做的唯一工作就是序列化。像json.Marshal和json.Unmarshal需要依赖运行时反射才能将结构体的字段序列化成字节,反之亦然。这个操作很慢,反射的性能完全无法与显式的代码相比。

但我们不必这么做。marshalling JSON的原理大致如下:

package json// Marshal take an object and returns its representation in JSON.func Marshal(obj interface{}) ([]byte, error) {    // Check if this object knows how to marshal itself to JSON    // by satisfying the Marshaller interface.    if m, is := obj.(json.Marshaller); is {        return m.MarshalJSON()    }    // It doesn't know how to marshal itself. Do default reflection based marshallling.    return marshal(obj)}

// Marshal take an object and returns its representation in JSON.func Marshal(obj interface{}) ([]byte, error) {    // Check if this object knows how to marshal itself to JSON    // by satisfying the Marshaller interface.    if m, is := obj.(json.Marshaller); is {        return m.MarshalJSON()    }

    // It doesn't know how to marshal itself. Do default reflection based marshallling.    return marshal(obj)}

如果我们知道怎样将对象marshal成JSON,就应该避免运行时反射。但我们不想手工marshal所有代码,怎么办呢?可以让计算机替我们写程序!像easyjson等代码生成器会检查结构体,然后生成高度优化且与json.Marshaller等接口完全兼容的代码。

下载这个包,然后在包含结构体的$file.go上运行下面的命令:

easyjson -all $file.go$file.go

这个命令会生成$file_easyjson.go。由于easyjson为我们实现了json.Marshaller接口,因此序列化时不会调用默认的反射,而是会使用生成的函数。祝贺你!你已经将JSON marshalling的代码的速度提高了三倍。还有许多其他技巧可以进一步提升性能。

我推荐这个包,是因为我之前用过,而且效果非常不错。不过请不要以此为契机跟我争论哪个JSON marshal的包最快。

需要确保在结构体改变后重新生成marshalling的代码。如果忘记,那么新的字段就不会被序列化和反序列化,会给编程造成困扰!可以调用go generate来处理代码生成。为了保证代码与结构体同步,我会在包的根目录下放一个generate.go文件,它会针对保重的所有文件调用go generate,这样可以帮助我处理众多需要生成的文件。小提示:在CI过程中调用go generate,检查生成的代码与已提交的代码是否有区别,来确保结构体是最新的。

使用strings.Builder来构建字符串

Go语言的字符串是不可修改的,可以认为它们是只读的字节切片。这就是说,每次创建字符串都要分配新的内存,可能还会给垃圾回收器造成更多工作。

Go 1.10引入了strings.Builder作为高效率构建字符串的方式。它内部会将字符串写入到字节缓冲区。只有在builder上调用String()时才会真正生成字符串。它依赖一些unsafe的技巧将底层的字节作为字符串返回,而不实际进行内存非配。这篇文章(https://syslog.ravelin.com/byte-vs-string-in-go-d645b67ca7ff)介绍了更多其工作原理。

我们来比较下两种方式的性能:

// main.gopackage mainimport "strings"var strs = []string{    "here's",    "a",    "some",    "long",    "list",    "of",    "strings",    "for",    "you",}func buildStrNaive() string {    var s string    for _, v := range strs {        s += v    }    return s}func buildStrBuilder() string {    b := strings.Builder{}    // Grow the buffer to a decent length, so we don't have to continually    // re-allocate.    b.Grow(60)    for _, v := range strs {        b.WriteString(v)    }    return b.String()}package main

import "strings"

var strs = []string{    "here's",    "a",    "some",    "long",    "list",    "of",    "strings",    "for",    "you",}

func buildStrNaive() string {    var s string

    for _, v := range strs {        s += v    }

    return s}

func buildStrBuilder() string {    b := strings.Builder{}

    // Grow the buffer to a decent length, so we don't have to continually    // re-allocate.    b.Grow(60)

    for _, v := range strs {        b.WriteString(v)    }

    return b.String()}
// main_test.gopackage mainimport (    "testing")var str stringfunc BenchmarkStringBuildNaive(b *testing.B) {    for i := 0; i < b.N; i++ {        str = buildStrNaive()    }}func BenchmarkStringBuildBuilder(b *testing.B) {    for i := 0; i < b.N; i++ {        str = buildStrBuilder()    }package main

import (    "testing")

var str string

func BenchmarkStringBuildNaive(b *testing.B) {    for i := 0; i < b.N; i++ {        str = buildStrNaive()    }}func BenchmarkStringBuildBuilder(b *testing.B) {    for i := 0; i < b.N; i++ {        str = buildStrBuilder()    }

在我的Macbook Pro上的结果如下:

? strbuild → go test -bench=. -benchmemgoos: darwingoarch: amd64pkg: github.com/sjwhitworth/perfblog/strbuildBenchmarkStringBuildNaive-8          5000000           255 ns/op         216 B/op          8 allocs/opBenchmarkStringBuildBuilder-8       20000000            54.9 ns/op        64 B/op          1 allocs/opgoos: darwingoarch: amd64pkg: github.com/sjwhitworth/perfblog/strbuildBenchmarkStringBuildNaive-8          5000000           255 ns/op         216 B/op          8 allocs/opBenchmarkStringBuildBuilder-8       20000000            54.9 ns/op        64 B/op          1 allocs/op

可见,strings.Builder要快4.7倍,它的内存分配次只有前者的1/8,内存使用只有前者的1/4。

所以,在性能重要的时候应该使用strings.Builder。一般来说,除非是非常不重要的情况,否则我建议永远使用strings.Builder来构建字符串。

使用strconv代替fmt

fmt是Go中最著名的包之一。估计你从第一个Go程序——输出“hello, world”——的时候就开始用它了。但是,如果需要将整数和浮点数转换成字符串,它就不如更底层的strconv有效率了。strconv只需要在API中进行很小改动,就能带来不错的性能提升。

大多数情况下fmt会接受一个interface{}作为参数。这样做有两个弊端:

  • 失去了类型安全。对于我来说这是个很大的问题。

  • 会增加内存分配次数。将非指针类型作为interface{}传递通常会导致堆分配的问题。进一步的内容可以阅读这篇文章(https://www.darkcoding.net/software/go-the-price-of-interface/)。

下面的程序显示了性能上的差异:

// main.gopackage mainimport (    "fmt"    "strconv")func strconvFmt(a string, b int) string {    return a + ":" + strconv.Itoa(b)}func fmtFmt(a string, b int) string {    return fmt.Sprintf("%s:%d", a, b)}func main() {}package main

import (    "fmt"    "strconv")

func strconvFmt(a string, b int) string {    return a + ":" + strconv.Itoa(b)}

func fmtFmt(a string, b int) string {    return fmt.Sprintf("%s:%d", a, b)}

func main() {}
// main_test.gopackage mainimport (    "testing")var (    a    = "boo"    blah = 42    box  = "")func BenchmarkStrconv(b *testing.B) {    for i := 0; i < b.N; i++ {        box = strconvFmt(a, blah)    }    a = box}func BenchmarkFmt(b *testing.B) {    for i := 0; i < b.N; i++ {        box = fmtFmt(a, blah)    }    a = box}package main

import (    "testing")

var (    a    = "boo"    blah = 42    box  = "")

func BenchmarkStrconv(b *testing.B) {    for i := 0; i < b.N; i++ {        box = strconvFmt(a, blah)    }    a = box}

func BenchmarkFmt(b *testing.B) {    for i := 0; i < b.N; i++ {        box = fmtFmt(a, blah)    }    a = box}

在Macbook Pro上的测试结果:

? strfmt → go test -bench=. -benchmemgoos: darwingoarch: amd64pkg: github.com/sjwhitworth/perfblog/strfmtBenchmarkStrconv-8      30000000            39.5 ns/op        32 B/op          1 allocs/opBenchmarkFmt-8          10000000           143 ns/op          72 B/op          3 allocs/opgoos: darwingoarch: amd64pkg: github.com/sjwhitworth/perfblog/strfmtBenchmarkStrconv-8      30000000            39.5 ns/op        32 B/op          1 allocs/opBenchmarkFmt-8          10000000           143 ns/op          72 B/op          3 allocs/op

可以看到,strconv版本要快3.5倍,内存分配次数是1/3,内存分配量是1/2。

在make中指定分配的容量来避免重新分配

在讨论性能改善之前,我们先来迅速看一下切片。切片是Go语言中一个非常有用的概念。它提供了可改变大小的数组,还可以用不同的方式表示同一片底层内存区域,而不需要重新进行内存分配。slice的内部结构由三个元素组成:

type slice struct {    // pointer to underlying data in the slice.    data uintptr    // the number of elements in the slice.    len int    // the number of elements that the slice can     // grow to before a new underlying array    // is allocated.    cap int     }struct {    // pointer to underlying data in the slice.    data uintptr    // the number of elements in the slice.    len int    // the number of elements that the slice can     // grow to before a new underlying array    // is allocated.    cap int     }

这些字段都是什么?

  • data:切片中指向底层数据的指针

  • len:切片中的当前元素数目

  • cap:在不重新分配内存的前提下,切片能够增长到的元素数目

在底层,切片是固定长度的数组。当长度增长到cap时,就会重新分配一个cap为原先两倍大小的数组,然后将原来的切片的内存区域拷贝到新的数组,最后释放旧的数组。

我经常看到类似于下面的代码,尽管在切片容量可以预先得知的情况下依然生成一个容量为零的切片:

var userIDs []stringfor _, bar := range rsp.Users {    userIDs = append(userIDs, bar.ID)}stringfor _, bar := range rsp.Users {    userIDs = append(userIDs, bar.ID)}

这段代码中,切片的初始长度和容量都为零。在收到响应后,我们将用户添加到切片。这样做就会达到切片的容量上限,从而导致底层分配两倍容量的新数组,然后将旧切片中的数据拷贝过来。如果有8个用户,就会造成5次内存分配。

更有效的方式是这样:

userIDs := make([]string, 0, len(rsp.Users)for _, bar := range rsp.Users {    userIDs = append(userIDs, bar.ID)}string, 0, len(rsp.Users)

for _, bar := range rsp.Users {    userIDs = append(userIDs, bar.ID)}

使用make显式声明切片的容量。接下来可以向切片添加元素,而不会触发内存重新分配和拷贝。

如果数量是动态的,或只能稍后计算,从而无法预先得知应该分配多少内存,可以先运行程序,然后测量一下内存大小的实际分布情况。我一般会取90或99百分位数,将这个数字写到程序中。如果你愿意用RAM换取CPU时间,可以设置更高的值。

这条建议也适用于map:适用make(map[string]string, len(foo))可以分配足够的底层内存以避免内存重新分配。

关于切片的工作原理可以参见这篇文章“Go切片:用法和内部原理”(https://blog.golang.org/go-slices-usage-and-internals)。

使用可以接受字节切片的方法

在使用包时,寻找那些接受字节切片作为参数的方法,这些方法通常给你更多控制内存分配的自由。

一个很好的例子就是time.Format和time.AppendFormat。time.Format返回字符串。内部会分配一个新的字节切片,然后在其上调用time.AppendFormat。而time.AppendFormat接受一个字节缓冲区,将格式化后的时间写入缓冲区,然后返回扩展后的字节切片。标准库中这种做法非常常见,如strconv.AppendFloat或bytes.NewBuffer。

为什么这样能提高性能?因为你可以传递从sync.Poolh获得的字节切片,而不需要每次都分配新的缓冲区。或者可以初始化一个足够大的缓冲区,来减少切片拷贝。

总结

读完这篇文章后,你应该可以在代码中应用这些技巧了。长期坚持这种做法,你就会习惯于考虑Go程序的性能。这会极大地影响你的设计能力。

作为结语,我需要提醒一点。我的这些建议只是某些具体情况下的建议,而不是真理。一定要自己测量性能。

要知道何时该停止优化。提高系统性能会让工程师感觉非常满足:问题本身很有趣,也有立竿见影的效果。但是,提高性能带来的效果非常依赖于具体情况。如果服务的响应时间只有10毫秒,而网络访问需要90毫秒,那么将10毫秒优化到5毫秒就完全不值得,因为你依然需要95毫秒。就算你将响应时间优化到1毫秒,最后结果还是91毫秒。你应该去做其他更有价值的事情。

用心优化!

原文:https://stephen.sh/posts/quick-go-performance-improvements

作者:Stephen Whitworth,软件工程师@monzo,https://www.ravelin.com/的合伙创始人。

本文为 CSDN 翻译,转载请注明来源出处。

【END】

 热 文 推 荐 

想换行做 5G 的开发者到底该咋办?

☞V神已抵京, 倒计时4天! 6大理由告诉你为什么要参加"2019以太坊技术及应用大会"

你还在抱怨开发工具,为什么不动手优化? | 人物志

☞全面认识 Android OS

☞盖茨承认犯下价值 4000 亿美元的错!

☞听说,私有云也出新一代了?

☞Flink最锋利的武器:Flink SQL入门和实战 | 附完整实现代码

☞Python处理图片九宫格,炫酷朋友圈

她说:程序员离开电脑就是 “废物” !

点击阅读原文,输入关键词,搜索CSDN文章。

你点的每个“在看”,我都认真当成了喜欢

如何快速提升 Go 程序性能?相关推荐

  1. go bson转换成json_如何快速提升 Go 程序性能?

    快速提升 Go 程序性能的实用技巧,你值得了解一下. 作者 | Stephen Whitworth 译者 | 弯月,责编 | 屠敏 出品 | CSDN(ID:CSDNnews) 我对软件性能的话题十分 ...

  2. 哪些模块可用于python性能分析_提升Python程序性能的方法有哪些?看完你就知道啦!...

    掌握一些技巧,可尽量提高Python程序性能,也可以避免不必要的资源浪费.今天就为大家带来七个可以提升python程序性能的好习惯,赶快来学习吧:. 1.使用局部变量 尽量使用局部变量代替全局变量:便 ...

  3. 【Python】提升Python程序性能的好习惯2

    掌握一些技巧,可尽量提高Python程序性能,也可以避免不必要的资源浪费. 追求性能极限是一个有趣的游戏, 而过度优化就会变成嘲弄了.虽然Python授予你与C接口无缝集成的能力, 你必须问自己你花数 ...

  4. 「技术架构」10个提升应用程序性能的倚天剑和屠龙刀

    提高web应用程序性能比以往任何时候都更加重要.在线经济活动的份额正在增长;超过5%的发达国家的经济现在是在互联网上的(参见参考资料中的互联网统计数据).而我们这个始终在线.高度连接的现代世界意味着用 ...

  5. 【Python】提升Python程序性能的好习惯

    掌握一些技巧,可尽量提高Python程序性能,也可以避免不必要的资源浪费. 追求性能极限是一个有趣的游戏, 而过度优化就会变成嘲弄了.虽然Python授予你与C接口无缝集成的能力, 你必须问自己你花数 ...

  6. 提升Python程序性能的7个习惯

    掌握一些技巧,可尽量提高Python程序性能,也可以避免不必要的资源浪费. 培训和发展 1.使用局部变量 尽量使用局部变量代替全局变量:便于维护,提高性能并节省内存. 使用局部变量替换模块名字空间中的 ...

  7. 微信小程序开发04 性能优化:借助微信开发者工具提升小程序性能

    你好,我是周俊鹏. 前几节课我们分别从架构层(双线程模型).链路层(授权模型).和应用层(自定义组件)三个角度学习了小程序的技术要点.它们能帮你完成一个微信小程序的基本业务逻辑和交互逻辑. 逻辑的第一 ...

  8. 如何为python程序设置使用次数_提升Python程序性能的7个习惯

    Python不以性能见长,但掌握一些技巧,也可尽量提高程序性能,避免不必要的资源浪费. 1.使用局部变量 尽量使用局部变量代替全局变量:便于维护,提高性能并节省内存. 使用局部变量替换模块名字空间中的 ...

  9. 利用 PGO 提升 .NET 程序性能

    引子 .NET 6 开始初步引入 PGO.PGO 即 Profile Guided Optimization,通过收集运行时信息来指导 JIT 如何优化代码,相比以前没有 PGO 时可以做更多以前难以 ...

最新文章

  1. 隐马尔科夫模型HMM自学 (3)
  2. 重启用reboot后起不来_2021年中国“天眼”开放,美媒记者探访后感叹了……
  3. SAP License:MM常用事物码
  4. 在ubuntu16上新创建了一个用户,通过xrdp远程连接时出现灰屏,鼠标是“x”号
  5. 性能分析工具Linux perf使用经验
  6. “Windows Sandbox”——PC主系统从未如此安全
  7. 静态反编译软件:IDA Pro for Mac
  8. Panabit标准版免费版功能限制
  9. 不足200行代码,我用python写了一个上课点名系统。还没开学的你们慌了吗?
  10. 模拟位置 定位 钉钉打卡 运动轨迹 MD
  11. 标签thead与th的区别
  12. TrustedInstaller
  13. centos7 安装hashcat
  14. 第三方服务之Bmob——快速入门
  15. Eclipse 开发Hadoop2.7.1可能会遇到的问题
  16. android打开位置服务,Android - 位置定位(Location)服务(Service)类的基本操作
  17. thinkpad X1 2016 NMV固态硬盘 win7+win10双系统 GPT+UEFI启动 系统安装记录
  18. 几款实用的内网穿透工具,推荐
  19. 5个UI设计师必备的Figma汉化插件
  20. 软件过程与项目管理(第一周作业)

热门文章

  1. Hive中表名、别名的限制
  2. SQL Server插入binary类型的数据
  3. Python+Opencv图像处理新手入门教程(二):颜色空间转换,图像大小调整,灰度直方图
  4. python while循环例题_【学习笔记】python:5for循环与while循环(上)
  5. Pytorch框架实战——102类花卉分类
  6. 第三章:变量与字符串等基础知识
  7. Dart基础-运算符
  8. 初中计算机考试成绩会纳入吗,【政策】北京海淀将信息技术纳入初中学业水平测试,考试不通过将不予毕业...
  9. Linux 免费学习路线大全,你想要的都在这里啦(持续更新,欢迎收藏❤️关注点赞加评论)
  10. IT-游戏 学习资源思维导图(持续更新,欢迎关注点赞加评论)