Go 重構筆記 3 - Regexp performance

很久沒寫文章了,雖然靈感很多,但最近因為新專案上線,和做一些有的沒的 side project,真的沒空整理資料和寫文章 都是藉口,所以上來刷個存在感,touch 一下 blog 的最後更新時間。

Background

專案有一段很長的 Regular expression 在判斷字串符不符合規則。

package main

import "regexp"

const p = `^([A-Za-z0-9_.-]+|[a-z0-9._%+\-][email protected][a-z0-9.\-]+\.[a-z]{2,4})$`

func validate(s string) (bool, error) {
	return regexp.MatchString(p, s)
}

測了一下才發現他的 performance 極差,只要每做一次,就會多 alloc 81 次記憶體,增加了很多 GC 壓力。

package main

import "testing"

func BenchmarkValidate(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_, _ = validate("foo")
	}
}
$ go test -bench=. -benchmem -benchtime=3s -run=none
BenchmarkValidate-4   311816        12025 ns/op       9246 B/op    81 allocs/op

Solution

可以使用 regexp.Compile() 讓它只 compile 一次就好,不需要每次 call function 都處理一次 regexp pattern。

package main

import "regexp"

const p = `^([A-Za-z0-9_.-]+|[a-z0-9._%+\-][email protected][a-z0-9.\-]+\.[a-z]{2,4})$`

func validate(s string) (bool, error) {
	return regexp.MatchString(p, s)
}

var r = regexp.MustCompile(p)

func validate2(s string) bool {
	return r.MatchString(s)
}

我們再測一次發現,效能差了好幾倍,本來會多 alloc 81 次記憶體變成 0 次,速度也快了 50 幾倍。

package main

import "testing"

func BenchmarkValidate(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_, _ = validate("foo")
	}
}

func BenchmarkValidate2(b *testing.B) {
	for i := 0; i < b.N; i++ {
		_ = validate2("foo")
	}
}
$ go test -bench=. -benchmem -benchtime=3s -run=none
BenchmarkValidate-4     298816     11231 ns/op    9241 B/op     81 allocs/op
BenchmarkValidate2-4  16597316       215 ns/op       0 B/op      0 allocs/op

後記

其實這是在學 Java 發現的 tips,剛好目前專案有遇到這個問題,很多地方都可以用這種方式處理,避免創建多次不必要的 instance,只在 compile 的時候做一次,多 reuse memory,可參考 Fasthttp best practices,當然因為都共用同一個 memory address,也要多考慮會不會發生 race condition。

掰。