欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文帶大家了解Go語言中的內(nèi)聯(lián)優(yōu)化

 更新時(shí)間:2023年05月30日 09:04:02   作者:趙不貪  
內(nèi)聯(lián)優(yōu)化是一種常見的編譯器優(yōu)化策略,通俗來講,就是把函數(shù)在它被調(diào)用的地方展開,這樣可以減少函數(shù)調(diào)用所帶來的開銷,本文主要為大家介紹了Go中內(nèi)聯(lián)優(yōu)化的具體使用,需要的可以參考下

內(nèi)聯(lián)優(yōu)化是一種常見的編譯器優(yōu)化策略,通俗來講,就是把函數(shù)在它被調(diào)用的地方展開,這樣可以減少函數(shù)調(diào)用所帶來的開銷(棧的創(chuàng)建、參數(shù)的拷貝等)。

當(dāng)函數(shù)/方法被內(nèi)聯(lián)時(shí),具體是什么樣的表現(xiàn)呢?

觀察內(nèi)聯(lián)

舉個(gè)例子,現(xiàn)在有以下代碼

// ValidateName 驗(yàn)證給定的用戶名是否合法
//
//go:noinline
func ValidateName(name string) bool { // AX: 字符串指針  BX: 字符串長度
    if len(name) < 1 {
        return false
    } else if len(name) > 12 {
        return false
    }
    return true
}
//go:noinline
func (s *Server) CreateUser(name string, password string) error {
    if !ValidateName(name) {
        return errors.New("invalid name")
    }
    // ...
    return nil
}
type Server struct{}

為了便于理解,我為函數(shù)和方法增加了//go:noinline注釋。Go編譯器在遇到該注釋時(shí),不會將函數(shù)/方法進(jìn)行內(nèi)聯(lián)處理。我們先看一下禁止內(nèi)聯(lián)時(shí),該段代碼生成的匯編指令:

// ...
// ValidateName函數(shù) 
// 此時(shí):
// AX寄存器: 指向name字符串?dāng)?shù)組的指針
// BX寄存器: name字符串的長度
TEXT github.com/bootun/example/user.ValidateName(SB) github.com/bootun/example/user/user.go
    user.go:9   0x4602c0  MOVQ AX, 0x8(SP) // 保存name字符串的指針到棧上(后面沒有用到)
    user.go:10  0x4602c5  TESTQ BX, BX     // BX & BX, 用來檢測BX是否為0, 等價(jià)于:CMPQ 0, BX
    user.go:10  0x4602c8  JE 0x4602d9      // 如果為0則跳轉(zhuǎn)到0x4602d9
    user.go:12  0x4602ca  CMPQ $0xc, BX    // 比較常數(shù)12和name的長度
    user.go:12  0x4602ce  JLE 0x4602d3     // 小于等于12則跳轉(zhuǎn)到0x4602d3
    user.go:13  0x4602d0  XORL AX, AX      // return false
    user.go:13  0x4602d2  RET
    user.go:15  0x4602d3  MOVL $0x1, AX    // return true
    user.go:15  0x4602d8  RET
    user.go:11  0x4602d9  XORL AX, AX      // return false
    user.go:11  0x4602db  RET
// CreateUser方法
TEXT github.com/bootun/example/user.(*Server).CreateUser(SB) /github.com/bootun/example/user/user.go
    // 省略了一些函數(shù)調(diào)用前的準(zhǔn)備工作(寄存器賦值等操作)
    user.go:20    0x460300  CALL user.ValidateName(SB)
    user.go:20    0x460305  TESTL AL, AL
    user.go:20    0x460307  JE 0x460317
    user.go:24    0x460309  XORL AX, AX
    user.go:24    0x46030b  XORL BX, BX
    user.go:24    0x46030d  MOVQ 0x10(SP), BP
    user.go:24    0x460312  ADDQ $0x18, SP
    user.go:24    0x460316  RET
    errors.go:62  0x460317  LEAQ 0x9302(IP), AX
    errors.go:62  0x46031e  NOPW
    errors.go:62  0x460320  CALL runtime.newobject(SB)
    // ...

上面的匯編里只截取了最關(guān)鍵的兩段: ValidateName函數(shù)和CreateUser方法。

看不懂匯編的同學(xué)也沒關(guān)系,注意看CreateUser方法內(nèi)有一行user.go:20 CALL user.ValidateName, 說明在CreateUser方法內(nèi)調(diào)用了ValidateName函數(shù),剛好和我們的代碼能夠?qū)?yīng)的上。

現(xiàn)在讓我們?nèi)サ粼创aValidateName函數(shù)上的//go:noinline再次編譯后查看生成的匯編指令:

如果你想使用文章里的代碼進(jìn)行嘗試,請不要?jiǎng)h除CreateUser方法上的//go:noinline,因?yàn)槔又械?code>CreateUser太簡短了,編譯器會把它也內(nèi)聯(lián)優(yōu)化掉,不方便我們進(jìn)行試驗(yàn)和觀察

// CreateUser函數(shù)
// 此時(shí):
// AX寄存器: 方法Recever,即Server結(jié)構(gòu)體
// BX寄存器: name字符串的指針
// CX寄存器: name字符串的長度
TEXT github.com/bootun/example/user.(*Server).CreateUser(SB) /github.com/bootun/example/user/user.go
    // ...              
    user.go:18    0x4602d4  MOVQ BX, 0x28(SP)    // 保存name字符串的指針到棧上
    user.go:19    0x4602d9  TESTQ CX, CX         // 驗(yàn)證name的長度是否為0
    user.go:9     0x4602dc  JE 0x4602e6          // 為0則跳轉(zhuǎn)到0x4602e6
    user.go:9     0x4602de  NOPW
    user.go:11    0x4602e0  CMPQ $0xc, CX        // 比較常數(shù)12和字符串的長度
    user.go:11    0x4602e4  JLE 0x460318         // 小于等于則跳轉(zhuǎn)到0x460318繼續(xù)執(zhí)行(name合法)
    errors.go:62  0x4602e6  LEAQ 0x9333(IP), AX  // 構(gòu)造錯(cuò)誤返回                   
    errors.go:62  0x4602ed  CALL runtime.newobject(SB)                  
    errors.go:62  0x4602f2  MOVQ $0xc, 0x8(AX)                    
    // ...
    user.go:23    0x460318  XORL AX, AX      // AX = 0
    user.go:23    0x46031a  XORL BX, BX      // BX = 0				
    user.go:23    0x46031c  MOVQ 0x10(SP), BP  // 恢復(fù)BP寄存器
    user.go:23    0x460321  ADDQ $0x18, SP     // 增加棧指針, 減小棧空間
    user.go:23    0x460325  RET                // return
    // ...

觀察這一次的代碼可以發(fā)現(xiàn),ValidateName函數(shù)的邏輯直接被內(nèi)嵌到了CreateUser方法里展開了。我們在生成的匯編代碼里也搜索不到ValidateName相關(guān)的符號了。 現(xiàn)在的代碼等價(jià)于:

func (s *Server) CreateUser(name string, password string) error {
    if len(name) < 1 {
        return errors.New("invalid name")
    } else if len(name) > 12 {
        return errors.New("invalid name")
    }
    return nil
}

什么樣的函數(shù)會被內(nèi)聯(lián)?

內(nèi)聯(lián)相關(guān)的代碼在cmd/compile/internal/inline/inl.go里,屬于編譯器的一部分。在該文件的最上面有這樣一段注釋, 里面很好的概括了內(nèi)聯(lián)的控制和規(guī)則:

// The Debug.l flag controls the aggressiveness. Note that main() swaps level 0 and 1,
// making 1 the default and -l disable. Additional levels (beyond -l) may be buggy and
// are not supported.
//      0: disabled
//      1: 80-nodes leaf functions, oneliners, panic, lazy typechecking (default)
//      2: (unassigned)
//      3: (unassigned)
//      4: allow non-leaf functions
//
// At some point this may get another default and become switch-offable with -N.
//
// The -d typcheckinl flag enables early typechecking of all imported bodies,
// which is useful to flush out bugs.
//
// The Debug.m flag enables diagnostic output.  a single -m is useful for verifying

總結(jié)一下上面這段話中的核心部分:

  • 80個(gè)節(jié)點(diǎn)的葉子函數(shù),oneliners,panic,懶惰的類型檢查 會被內(nèi)聯(lián)
  • 使用-N -l來告訴編譯器禁止內(nèi)聯(lián)
  • 使用-m啟用診斷輸出

也就是說,只要我們的函數(shù)/方法足夠小,就可能會被內(nèi)聯(lián)。 因此,很多人會使用許多小的函數(shù)組合來代替大段代碼提升性能。比如我們經(jīng)常使用的互斥鎖(標(biāo)準(zhǔn)庫中sync包里的Mutex)就利用了這一點(diǎn), 我們平時(shí)使用的Lock方法一共就只有這幾行:

func (m *Mutex) Lock() {
    // Fast path: grab unlocked mutex.
    if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
        if race.Enabled {
            race.Acquire(unsafe.Pointer(m))
        }
        return
    }
    // Slow path (outlined so that the fast path can be inlined)
    m.lockSlow()
}

注意倒數(shù)第三行的注釋:outlined so that the fast path can be inlined,利用這一特性,Lock里的FastPath就能被內(nèi)聯(lián)到我們的程序里而不需要額外的函數(shù)調(diào)用,從而提升代碼的性能。

函數(shù)內(nèi)聯(lián)部分的入口是函數(shù)inline.InlinePackage, 想要深入了解的小伙伴可以去看一看。

內(nèi)聯(lián)能為我的程序帶來多少性能上的提升?

前面介紹了這么多內(nèi)聯(lián),連標(biāo)準(zhǔn)庫都刻意使用內(nèi)聯(lián)來提升Go程序的性能,那么內(nèi)聯(lián)究竟能為我們帶來多少性能上的提升呢?

我們來擴(kuò)充一下文章開篇提到的例子:

package user
import (
      "errors"
)
func ValidateName(name string) bool {
      if len(name) < 1 {
            return false
      } else if len(name) > 12 {
            return false
      }
      return true
}
//go:noinline
func ValidateNameNoInline(name string) bool {
      if len(name) < 1 {
            return false
      } else if len(name) > 12 {
            return false
      }
      return true
}
func (s *Server) CreateUser(name string, password string) error {
      if !ValidateName(name) {
            return errors.New("invalid name")
      }
      return nil
}
// CreateUserNoInline 使用的是禁止內(nèi)聯(lián)版本的 ValidateName
func (s *Server) CreateUserNoInline(name string, password string) error {
      if !ValidateNameNoInline(name) {
            return errors.New("invalid name")
      }
      return nil
}
type Server struct{}

我們復(fù)制了以ValidateName函數(shù),在上面標(biāo)注上//go:noinline來禁止編譯器對其進(jìn)行內(nèi)聯(lián)優(yōu)化,并將其并將其更名為ValidateNameNoInline。同時(shí)我們也復(fù)制了CreateUser方法,新的方法內(nèi)部使用ValidateNameNoInline來驗(yàn)證name參數(shù),除此之外所有的地方都和原方法相同。

我們來寫兩個(gè)Benchmark測試一下:

package user
import "testing"
// BenchmarkCreateUser 測試內(nèi)聯(lián)過的函數(shù)的性能
func BenchmarkCreateUser(b *testing.B) {
      srv := Server{}
      for i := 0; i < b.N; i++ {
            if err := srv.CreateUser("bootun", "123456"); err != nil {
                  b.Logf("err: %v", err)
            }
      }
}
// BenchmarkValidateNameNoInline 測試函數(shù)禁止內(nèi)聯(lián)后的性能
func BenchmarkValidateNameNoInline(b *testing.B) {
      srv := Server{}
      for i := 0; i < b.N; i++ {
            if err := srv.CreateUserNoInline("bootun", "123456"); err != nil {
                  b.Logf("err: %v", err)
            }
      }
}

測試結(jié)果如下:

# 內(nèi)聯(lián)版本的基準(zhǔn)測試結(jié)果(BenchmarkCreateUser)
goos: windows
goarch: amd64
pkg: github.com/bootun/example/user
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkCreateUser
BenchmarkCreateUser-16          1000000000               0.2279 ns/op
PASS


# 禁止內(nèi)聯(lián)版本的基準(zhǔn)測試結(jié)果(BenchmarkValidateNameNoInline)
goos: windows
goarch: amd64
pkg: github.com/bootun/example/user
cpu: AMD Ryzen 7 6800H with Radeon Graphics
BenchmarkValidateNameNoInline
BenchmarkValidateNameNoInline-16        733243102                1.635 ns/op
PASS

可以看到,禁止內(nèi)聯(lián)后每次操作耗費(fèi)1.6納秒,而內(nèi)聯(lián)后只需要0.22納秒(因機(jī)器而異)。從比例上看,內(nèi)聯(lián)優(yōu)化帶來的收益還是很可觀的。

我需要做什么來啟用內(nèi)聯(lián)優(yōu)化嗎

當(dāng)然不需要,在Go編譯器中,內(nèi)聯(lián)優(yōu)化是默認(rèn)啟用的,如果你的函數(shù)符合文中提到的內(nèi)聯(lián)優(yōu)化的策略(比如函數(shù)很小),并且沒有顯式的禁用內(nèi)聯(lián),就可能會被編譯器執(zhí)行內(nèi)聯(lián)優(yōu)化。

在某些場景下,我們可能不希望函數(shù)進(jìn)行內(nèi)聯(lián)(比如使用dlv進(jìn)行DEBUG時(shí),或者查看程序生成的匯編代碼時(shí)),可以使用go build -gcflags='-N -l' xxx.go來禁用內(nèi)聯(lián)優(yōu)化。

編譯器默認(rèn)優(yōu)化出來的代碼可能比較難以閱讀和理解,不方便我們進(jìn)行調(diào)試和學(xué)習(xí)。

-gcflags是傳遞給go編譯器gc的命令行標(biāo)志, go build 背后做了很多事,也不止用到了gc一個(gè)程序。使用go build -x main.go可以查看編譯過程中的詳細(xì)步驟。

以上就是一文帶大家了解Go語言中的內(nèi)聯(lián)優(yōu)化的詳細(xì)內(nèi)容,更多關(guān)于Go內(nèi)聯(lián)優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論