优化一个已有的 Go 程序,提高其性能并减少资源占用 | 青训营

66 阅读4分钟

这是一篇后端的实践文章,主要内容是将已有的Go程序进行优化,提高程序的性能,并且在此基础上减少资源的占用。有错误之处,还请大家指正。

在实际的项目开发过程中,对于程序性能和资源的占用方面的优化是很重要的一部分工作,同时也是很具有挑战性的一项工作,尤其是在高并发场景,和处理大量数据的情况下。

下面举一个计算斐波那契数列的第n个数例子,进一步展示通过减少内存的使用实现对于Go程序的性能优化,其函数为:

func fib(n int) int { if n <= 1 { return n } return fib(n-1) + fib(n-2) }

这个程序的实现采用了递归算法,递归算法存在着一个明显的问题,就是在程序运行过程中,需要系统为其频繁地分配内存空间,占用了很多的内存空间,若同时开始很多的递归程序可能导致系统崩溃,无法执行下去,本例中一层一层的迭代会导致运行时间比较长,可以考虑使用数组,用来缓存在程序运行过程中已经计算得出的斐波那契数列的值,这样可以有效的减少内存的分配,代码展示如下:

package main

var fibCache map[int]int
func fib(n int) int { 
    if n <= 1 { 
        return n } 
    result := fib(n-1) + fib(n-2) 
    fibCache[n] = result 
    return result 
}

以上的代码中,使用了以下的优化方法,在运行中缓存了计算的中间结果,避免了重复的进行计算,有效的减少了内存的分配,这样的一些操作,使得优化后的代码可以较快的计算出较大的斐波那契数的值。这个例子说明,减少不必要的内存分配可以有效的提升程序的性能。

上述例子是一个较为简单的程序,下面将详细阐述对程序进行优化的三个步骤

一、性能分析、瓶颈定位

要对已有的程序进行优化,首先要对其进行性能分析,分析其瓶颈所在才可以对其实现进一步的优化操作,可以使用Go内置的工具,如pproftrace。该工具通过分析CPU和内存的使用情况,使我们可以快速找到程序的性能瓶颈,代码示例如下。

package main

import (
	"fmt"
	"log"
	"net/http"
	_ "net/http/pprof"
	"time"
)

func main() {
	go func() {
		log.Println(http.ListenAndServe("localhost:6060", nil))
	}()

	for i := 0; i < 100000; i++ {
		processData()
	}
}

func processData() {
	time.Sleep(10 * time.Millisecond)
}

上述代码里使用了net/http/pprof包用于启动HTTP服务器,实现pprof和trace数据的接收。

processData函数模拟了耗时操作。

运行程序并访问http://localhost:6060/debug/pprof/,就可以查看CPU以及内存的使用情况。

二、瓶颈优化

在第一步的基础上,我们才可以实现针对性的瓶颈优化,常采用的方法有以下几种:

2.1 并发、并行

在第一节的课程中,我们得知,Go语言是支持并发和并行的,可以使用goroutine和channel实现,并发、并行的存在可以使得程序充分的利用多核CPU资源,有效的提高了程序的处理能力,实例代码如下:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	numWorkers := 4
	numTasks := 100000

	var wg sync.WaitGroup
	wg.Add(numWorkers)

	taskQueue := make(chan int, numTasks)

	for i := 0; i < numWorkers; i++ {
		go worker(&wg, taskQueue)
	}

	for i := 0; i < numTasks; i++ {
		taskQueue <- i
	}
	close(taskQueue)

	wg.Wait()
}

func worker(wg *sync.WaitGroup, tasks <-chan int) {
	defer wg.Done()

	for task := range tasks {
		processTask(task)
	}
}

func processTask(task int) {
	time.Sleep(10 * time.Millisecond)
	fmt.Println("Processed task:", task)
}

2.2 内存的分配和回收

从本文的第一个斐波那契的例子可以得知,我们要尽量避免频繁的内存分配和回收,从而实现提高程序性能的目的,在以下的代码中,使用了runtime.GC(),手动的触发垃圾回收机制,以减少GC的负担,在一些情况下,这也是提高程序性能的一种方法。

package main

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

func main() {
	numIterations := 100000
	runtime.GC()

	start := time.Now()

	for i := 0; i < numIterations; i++ {
		processData()
	}

	elapsed := time.Since(start)
	fmt.Printf("Time taken: %s\n", elapsed)
}

func processData() {
	time.Sleep(10 * time.Millisecond)
}

2.3 减少锁竞争

锁竞争有可能导致程序的性能出现下降,考虑采用更细粒度的锁,或者使用无锁数据结构代替,以此减少锁竞争,从而实现程序性能的优化。

三、测试、比较

当我们采取一些操作对程序进行优化后,我们需要对其进行测试,以便检查是否达到优化的目标,可以使用测试工具testing来对优化前后的性能进行比较,代码展示如下。

package main

import (
	"testing"
	"time"
)

func BenchmarkProcessData(b *testing.B) {
	for i := 0; i < b.N; i++ {
		processData()
	}
}

经过以上一些列的操作,可以实现对于Go语言程序的优化过程,通过瓶颈优化部分的三个方法可以提升程序的效率,并且减少内存空间的占用