Go defer(二):從匯編的角度理解延遲調用的實現

Go的延遲調用機制會在當前函數返回前執行傳入的函數,它會經常被用于關閉文件描述符、關閉數據庫連接以及解鎖資源。之前的文章(?Go defer(一):延遲調用的使用及其底層實現原理詳解?)詳細介紹了defer的使用以及其底層實現原理,本文則以Go 1.16版本以及AMD 64架構為實驗環境,從匯編語言的角度去分析defer的實現原理,主要涉及defer參數傳遞方式、閉包、返回值修改、內存分配方式等內容。

1 使用特性

1.1 函數傳參

在使用defer實現延遲調用的時候,需要特別注意的是注冊函數的參數傳參方式,如果直接使用的常規的函數傳參,那么參數值在函數注冊到defer鏈表上時就已經被確定了,這時候延遲的只是函數調用的時機,參數值早在一開始構建_defer結構體的時候就已經確定了(如果是引用語義的類型,則需要特別注意,這里僅以int類型作為參數進行分析)。以下面的sum函數為例,分析其匯編代碼不難發現:

  • _defer結構體的內存布局會將注冊函數所需的參數、返回值所需的內存空間緊挨著_defer結構體。這里需要特別注意下,在_defer結構體的內存布局中,前8個字節包含siz(4字節)、started(1字節)、heap(1字節)、openDefer(1字節)。關于Go結構體內存布局,感興趣的可以參考下Go struct:結構體使用基礎以及內存布局。
  • 按照函數傳參的方式進行defer注冊,編譯器直接將sum函數所需要的參數值賦值到了_defer結構體存儲func參數的棧空間(值拷貝)
  • 由于函數傳參的方式是以值拷貝實現的,因此后續代碼執行的變量自增操作不會對之前延遲注冊函數所需的參數產生影響。(注意:Go的引用類型,slice、map、chan、interface,這些類型由于底層包含指針,雖然是值拷貝,但受指針影響,后續的修改可能會對延遲調用產生影響
func sum(a, b int) int {return a + b
}func main() {a, b := 1, 2// 通過函數傳參的方式注冊的延遲調用,在注冊時已經將函數的形參值確定了defer sum(a, b) a++
}"".main STEXT size=197 args=0x0 locals=0x90 funcid=0x00x0000 00000 (main.go:20)       TEXT    "".main(SB), ABIInternal, $144-0...0x002f 00047 (main.go:22)       MOVQ    $1, "".a+32(SP) // 變量a0x0038 00056 (main.go:22)       MOVQ    $2, "".b+24(SP) // 變量b0x0041 00065 (main.go:23)       MOVL    $24, ""..autotmp_2+40(SP) // SP+40~SP+112的棧空間存儲的是 _defer結構體0x0049 00073 (main.go:23)       LEAQ    "".sum·f(SB), AX0x0050 00080 (main.go:23)       MOVQ    AX, ""..autotmp_2+64(SP)0x0055 00085 (main.go:23)       MOVQ    "".a+32(SP), AX0x005a 00090 (main.go:23)       MOVQ    AX, ""..autotmp_2+112(SP)0x005f 00095 (main.go:23)       MOVQ    "".b+24(SP), AX0x0064 00100 (main.go:23)       MOVQ    AX, ""..autotmp_2+120(SP)0x0069 00105 (main.go:23)       LEAQ    ""..autotmp_2+40(SP), AX0x006e 00110 (main.go:23)       MOVQ    AX, (SP)0x0072 00114 (main.go:23)       PCDATA  $1, $00x0072 00114 (main.go:23)       CALL    runtime.deferprocStack(SB)0x0077 00119 (main.go:23)       TESTL   AX, AX // 測試返回值(AX寄存器),如果deferprocStack調用成功,return0()會將AX設置為00x0079 00121 (main.go:23)       JNE     161 // 如果返回值不為0,則跳轉到161,不再執行main函數中的后續代碼0x007b 00123 (main.go:23)       JMP     1250x007d 00125 (main.go:27)       MOVQ    "".a+32(SP), AX0x0082 00130 (main.go:27)       INCQ    AX // a++0x0085 00133 (main.go:27)       MOVQ    AX, "".a+32(SP)0x008a 00138 (main.go:28)       XCHGL   AX, AX0x008b 00139 (main.go:28)       CALL    runtime.deferreturn(SB) // 執行延遲調用0x0090 00144 (main.go:28)       MOVQ    136(SP), BP // 恢復調用者的BP0x0098 00152 (main.go:28)       ADDQ    $144, SP // 清理棧空間0x009f 00159 (main.go:28)       NOP0x00a0 00160 (main.go:28)       RET0x00a1 00161 (main.go:23)       XCHGL   AX, AX0x00a2 00162 (main.go:23)       CALL    runtime.deferreturn(SB)0x00a7 00167 (main.go:23)       MOVQ    136(SP), BP0x00af 00175 (main.go:23)       ADDQ    $144, SP0x00b6 00182 (main.go:23)       RET0x00b7 00183 (main.go:23)       NOP...

1.2 閉包

對于傳統值拷貝的類型,想要在defer執行函數時,實時獲取最新的參數值,則可以借助Go語言里面的閉包特性進行實現。閉包特性簡單來說就是使用指針取代具體值進行傳遞,這樣在后續需要使用該變量時,通過對地址的解引用來得到實時的值,從而實現變量最新值的獲取。

閉包=函數地址 + 引用變量的地址

當函數引用外部作用域的變量時,我們稱之為閉包。在底層實現上,閉包由函數地址和引用到的變量的地址組成,并存儲在一個結構體里,在閉包被傳遞時,實際是該結構體的地址被傳遞。

type Closure struct {F func()()   // 函數地址 uintptri *int       // 引用變量的地址
}

還是以sum函數為例,其中兩個參數a,b,采用閉包的方式構建匿名函數注冊延遲調用,分析其匯編語言發現,主要的流程大致與函數傳參的方式一致,區別在于:

  • 此時defer注冊的是匿名函數func1,不再是sum函數,新函數僅需要兩個變量,而沒有返回值
  • func1函數所需的參數依舊保存在_defer結構體之后,只是編譯器通過分析代碼,對不同處境的變量做了不同的處理:
    • 對于后續不會再被改變的變量,編譯器直接進行了值拷貝,例如此例中的變量b
    • 如果后續還會對變量進行修改,則編譯器將其內存地址保存到了_defer結構體之后,例如此例中的變量a
  • 由于參數a保存的是其地址,那么在執行func1時,對其進行解引用拿到的值就是執行了自增之后的值,既實際sum函數執行時,兩個參數的值a=2,b=2。
func sum(a, b int) int {return a + b
}func main() {a, b := 1, 2defer func() {sum(a, b)}()a++
}"".main STEXT size=170 args=0x0 locals=0x80 funcid=0x00x0000 00000 (main.go:20)       TEXT    "".main(SB), ABIInternal, $128-0...0x0021 00033 (main.go:22)       MOVQ    $1, "".a+24(SP)0x002a 00042 (main.go:22)       MOVQ    $2, "".b+16(SP)0x0033 00051 (main.go:23)       MOVL    $16, ""..autotmp_2+32(SP) // 此時注冊的延遲調用函數為func匿名函數,僅有sum函數的兩個變量,沒有返回值0x003b 00059 (main.go:23)       LEAQ    "".main.func1·f(SB), AX0x0042 00066 (main.go:23)       MOVQ    AX, ""..autotmp_2+56(SP)0x0047 00071 (main.go:23)       LEAQ    "".a+24(SP), AX // 由于后面的代碼還會對a進行修改,所以此處保存的是a的內存地址(閉包)0x004c 00076 (main.go:23)       MOVQ    AX, ""..autotmp_2+104(SP)0x0051 00081 (main.go:23)       MOVQ    "".b+16(SP), AX // b在后續不會再被修改,所以編譯器進行了優化直接存儲b的值0x0056 00086 (main.go:23)       MOVQ    AX, ""..autotmp_2+112(SP)0x005b 00091 (main.go:23)       LEAQ    ""..autotmp_2+32(SP), AX0x0060 00096 (main.go:23)       MOVQ    AX, (SP)0x0064 00100 (main.go:23)       PCDATA  $1, $00x0064 00100 (main.go:23)       CALL    runtime.deferprocStack(SB)0x0069 00105 (main.go:23)       TESTL   AX, AX // 判斷deferprocStack函數是否正常執行return0(),如果沒有正常執行直接跳轉至143進行棧的清理工作0x006b 00107 (main.go:23)       JNE     1430x006d 00109 (main.go:23)       JMP     1110x006f 00111 (main.go:26)       MOVQ    "".a+24(SP), AX0x0074 00116 (main.go:26)       INCQ    AX0x0077 00119 (main.go:26)       MOVQ    AX, "".a+24(SP)0x007c 00124 (main.go:27)       XCHGL   AX, AX0x007d 00125 (main.go:27)       NOP0x0080 00128 (main.go:27)       CALL    runtime.deferreturn(SB)0x0085 00133 (main.go:27)       MOVQ    120(SP), BP0x008a 00138 (main.go:27)       SUBQ    $-128, SP0x008e 00142 (main.go:27)       RET0x008f 00143 (main.go:23)       XCHGL   AX, AX0x0090 00144 (main.go:23)       CALL    runtime.deferreturn(SB)0x0095 00149 (main.go:23)       MOVQ    120(SP), BP0x009a 00154 (main.go:23)       SUBQ    $-128, SP0x009e 00158 (main.go:23)       RET...

1.3 返回值修改

defer注冊的函數是在主流程結束,函數返回之前被調用,那么如果在defer延遲調用的函數中對返回值進行修改,又會有怎么樣的現象呢?從匯編的角度來看,return分為兩步:先是對返回值進行賦值,最后函數結束時執行一個空的返回操作,而defer的執行時機則穿插在這兩步之間。具體的執行流程如下:

  • 返回值 = xxx

  • 調用defer注冊的函數

  • 空的return

根據上述的流程不難發現,可以在defer注冊的延遲調用函數內部對返回值進行修改或賦值,下面從返回值重新賦值、直接修改返回值兩種情況來分析下defer的延遲調用對返回值的影響。

1.3.1 返回值重新賦值

編寫如下的代碼,主要流程為定義一個變量t,通過閉包的形式對其在defer匿名函數中進行修改,并將該變量作為返回值進行返回。此函數有一個有名返回值r,既最后返回的值是一個內部的新變量,并不是直接定義的返回值變量r。通過匯編語言來分析下變量定義以及defer注冊之后,其函數棧的具體情況:

  • 首先,Go的函數棧分布情況為:調用者函數棧幀會為被調用者預留參數、返回值所需的內存空間。如下圖所示,main函數棧幀中會預留f函數的返回值空間。
  • 隨后,進入f函數內部,有一個局部變量t,以及在棧上分配的_defer結構體,該_defer的延遲調用函數為匿名f.func1,所需參數為t(由于閉包,存儲的是t的內存地址)

// 該函數返回值為5
func  f() (r int){t := 5defer func(){t = t + 5}()return t
}"".f STEXT size=155 args=0x8 locals=0x68 funcid=0x0...0x0021 00033 (main.go:4)        MOVQ    $0, "".r+112(SP) // 調用者的函數棧空間,用于存儲f函數的返回值0x002a 00042 (main.go:5)        MOVQ    $5, "".t+8(SP)0x0033 00051 (main.go:6)        MOVL    $8, ""..autotmp_2+16(SP)0x003b 00059 (main.go:6)        LEAQ    "".f.func1·f(SB), AX0x0042 00066 (main.go:6)        MOVQ    AX, ""..autotmp_2+40(SP)0x0047 00071 (main.go:6)        LEAQ    "".t+8(SP), AX0x004c 00076 (main.go:6)        MOVQ    AX, ""..autotmp_2+88(SP)0x0051 00081 (main.go:6)        LEAQ    ""..autotmp_2+16(SP), AX0x0056 00086 (main.go:6)        MOVQ    AX, (SP)0x005a 00090 (main.go:6)        PCDATA  $1, $00x005a 00090 (main.go:6)        CALL    runtime.deferprocStack(SB)0x005f 00095 (main.go:6)        NOP

?

隨后,代碼開始執行return語句:

  • 將t值賦值給r作為返回值,從匯編語言的角度看就是將t的值拷貝到r的內存空間(調用者預留的空間內)。
  • 開始執行延遲調用函數,既f.func1函數,將t的變量值?5,此處操作與變量r完全沒有關系。
  • 最后執行一個空的返回操作。
        0x0060 00096 (main.go:6)        TESTL   AX, AX0x0062 00098 (main.go:6)        JNE     1290x0064 00100 (main.go:6)        JMP     1020x0066 00102 (main.go:9)        MOVQ    "".t+8(SP), AX 0x006b 00107 (main.go:9)        MOVQ    AX, "".r+112(SP) // 將t的值賦值給r,既將其存儲到調用者的f函數返回值棧空間內0x0070 00112 (main.go:9)        XCHGL   AX, AX0x0071 00113 (main.go:9)        CALL    runtime.deferreturn(SB) // 執行延遲調用,將t的值?50x0076 00118 (main.go:9)        MOVQ    96(SP), BP0x007b 00123 (main.go:9)        ADDQ    $104, SP0x007f 00127 (main.go:9)        NOP0x0080 00128 (main.go:9)        RET     // 相當于執行空的return操作..."".f.func1 STEXT nosplit size=21 args=0x8 locals=0x0 funcid=0x00x0000 00000 (main.go:6)        TEXT    "".f.func1(SB), NOSPLIT|ABIInternal, $0-80x0000 00000 (main.go:6)        FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)0x0000 00000 (main.go:6)        FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)0x0000 00000 (main.go:7)        MOVQ    "".&t+8(SP), AX // AX = 捕獲變量 t 的地址0x0005 00005 (main.go:7)        MOVQ    "".&t+8(SP), CX // CX = 捕獲變量 t 的地址0x000a 00010 (main.go:7)        MOVQ    (CX), CX        // CX = *CX (解引用獲取 t 的值)0x000d 00013 (main.go:7)        ADDQ    $5, CX          // CX = t + 50x0011 00017 (main.go:7)        MOVQ    CX, (AX)        // *AX = CX (將結果寫回 t)0x0014 00020 (main.go:8)        RET

1.3.2 直接修改返回值

本例在之前的代碼基礎上進行修改,將返回值的名稱修改為t,既函數內部不再重新定義一個新的變量,而是直接對返回值進行操作,此時編譯器做出如下動作:

  • 首先,main函數棧幀中會預留f函數的返回值t,f函數對t的操作都直接會反應到返回值上。
  • 隨后,在棧上分配的_defer結構體,該_defer的延遲調用函數為匿名f.func1,所需參數為t(由于閉包,存儲的是t的內存地址,既返回值的地址)
  • 最后,deferreturn函數執行延遲調用時,執行匿名f.func1函數,會通過內存地址解引用找到t,對其進行?5操作,這里的修改直接反映在f.func1函數的返回值上,因此最終f函數會返回10。

2 實現方式

2.1 堆分配

隨著Go語言的不斷優化發展,_defer結構體內存分配方式也不再僅有最原始的堆分配,新增了棧分配方式以及開發編碼。雖然棧分配相比堆分配,性能占有,但其適用范圍有效,而堆分配則適用于所用的情況,下面以循環內部注冊defer觸發編譯時期無法確定具體defer數量為例,分析下此時函數棧、堆的具體情況:

  • 首先,由于_defer結構體所需的內存是在堆上進行分配的,那么函數棧幀中僅需存儲調用deferproc所需的參數值、defer延遲調用函數所需的參數以及返回值空間。
  • 隨后,調用deferproc函數,通過newdefer函數在堆上分配一塊_defer結構體內存,將參數拷貝到對應位置,同時在defer結構體常規成員變量之后。緊接著拷貝注冊的延遲調用函數所需參數以及返回值,并將該_defer結構體加入到Goroutine到_defer鏈表頭部。
func sum(a, b int) int {return a + b
}func main() {for i := 0; i < 3; i++ {// 循環內的 defer 導致無法在編譯時確定數量defer sum(i, i+1)}
}"".main STEXT size=170 args=0x0 locals=0x38 funcid=0x0...0x0021 00033 (main.go:9)        MOVQ    $0, "".i+40(SP) // 變量i初始化0x002a 00042 (main.go:9)        JMP     440x002c 00044 (main.go:9)        CMPQ    "".i+40(SP), $3 // i與3進行比較0x0032 00050 (main.go:9)        JLT     540x0034 00052 (main.go:9)        JMP     1430x0036 00054 (main.go:11)       MOVQ    "".i+40(SP), AX0x003b 00059 (main.go:11)       MOVQ    "".i+40(SP), CX0x0040 00064 (main.go:11)       MOVL    $24, (SP) // deferproc 函數的第一個參數siz0x0047 00071 (main.go:11)       LEAQ    "".sum·f(SB), DX0x004e 00078 (main.go:11)       MOVQ    DX, 8(SP) // deferproc 函數的第二個參數fn0x0053 00083 (main.go:11)       MOVQ    AX, 16(SP) // "".sum·f 函數的第一個參數0x0058 00088 (main.go:11)       LEAQ    1(CX), AX // AX = i+10x005c 00092 (main.go:11)       MOVQ    AX, 24(SP) // "".sum·f 函數的第二個參數0x0061 00097 (main.go:11)       PCDATA  $1, $00x0061 00097 (main.go:11)       CALL    runtime.deferproc(SB) // 調用 deferproc函數從堆上分配_defer所需的內存0x0066 00102 (main.go:11)       TESTL   AX, AX // return0()函數是否執行成功0x0068 00104 (main.go:11)       JNE     1250x006a 00106 (main.go:11)       JMP     1080x006c 00108 (main.go:9)        PCDATA  $1, $-10x006c 00108 (main.go:9)        JMP     1100x006e 00110 (main.go:9)        MOVQ    "".i+40(SP), AX0x0073 00115 (main.go:9)        INCQ    AX0x0076 00118 (main.go:9)        MOVQ    AX, "".i+40(SP)0x007b 00123 (main.go:9)        JMP     440x007d 00125 (main.go:11)       PCDATA  $1, $00x007d 00125 (main.go:11)       XCHGL   AX, AX0x007e 00126 (main.go:11)       NOP0x0080 00128 (main.go:11)       CALL    runtime.deferreturn(SB)0x0085 00133 (main.go:11)       MOVQ    48(SP), BP0x008a 00138 (main.go:11)       ADDQ    $56, SP0x008e 00142 (main.go:11)       RET...

2.2 棧分配

棧分配適用的條件沒有堆分配那么多,僅適用于函數不逃逸且defer?數量確定的場景,由于此時_defer結構體是在函數棧幀上分配的,那么只需移動SP的值就可以完成_defer結構體內存的回收,執行效率很高。對比堆分配,棧分配只是將內存空間放在了棧上,_defer內存布局、延遲調用函數參數及返回值存儲位置與堆分配完全一致。

func sum(a, b int) int {return a + b
}func main() {defer sum(1, 2) 
}"".main STEXT size=126 args=0x0 locals=0x80 funcid=0x0...0x001d 00029 (main.go:8)        MOVL    $24, ""..autotmp_0+24(SP)0x0025 00037 (main.go:8)        LEAQ    "".sum·f(SB), AX0x002c 00044 (main.go:8)        MOVQ    AX, ""..autotmp_0+48(SP)0x0031 00049 (main.go:8)        MOVQ    $1, ""..autotmp_0+96(SP)0x003a 00058 (main.go:8)        MOVQ    $2, ""..autotmp_0+104(SP)0x0043 00067 (main.go:8)        LEAQ    ""..autotmp_0+24(SP), AX0x0048 00072 (main.go:8)        MOVQ    AX, (SP)0x004c 00076 (main.go:8)        PCDATA  $1, $00x004c 00076 (main.go:8)        CALL    runtime.deferprocStack(SB)...

2.3 開放編碼

使用go tool compile -S -l main.go 命令查看了下述簡單代碼在開放編碼下的匯編語言,可以發現編譯器沒有再調用deferprocStack、deferproc去為每個defer生成一個defer結構體,而是直接編譯成函數調用的方式,相當于把延遲調用改寫成了在函數返回之前需要進行的正常函數調用。

func sum(a, b int) int {return a + b
}func main() {defer sum(1, 2)
}$  go tool compile -S -l main.go "".main STEXT size=140 args=0x0 locals=0x40 funcid=0x0...0x0029 00041 (main.go:11)       FUNCDATA        $4, "".main.opendefer(SB) // 標記使用了開放編碼0x0029 00041 (main.go:11)       MOVB    $0, ""..autotmp_0+31(SP)0x002e 00046 (main.go:12)       LEAQ    "".sum·f(SB), AX0x0035 00053 (main.go:12)       MOVQ    AX, ""..autotmp_1+48(SP)0x003a 00058 (main.go:12)       MOVQ    $1, ""..autotmp_2+40(SP)0x0043 00067 (main.go:12)       MOVQ    $2, ""..autotmp_3+32(SP)0x004c 00076 (main.go:13)       MOVB    $0, ""..autotmp_0+31(SP)0x0051 00081 (main.go:13)       MOVQ    ""..autotmp_2+40(SP), AX0x0056 00086 (main.go:13)       MOVQ    ""..autotmp_3+32(SP), CX0x005b 00091 (main.go:13)       MOVQ    AX, (SP)0x005f 00095 (main.go:13)       MOVQ    CX, 8(SP)0x0064 00100 (main.go:13)       PCDATA  $1, $10x0064 00100 (main.go:13)       CALL    "".sum(SB) // 直接進行了函數調用0x0069 00105 (main.go:13)       MOVQ    56(SP), BP0x006e 00110 (main.go:13)       ADDQ    $64, SP0x0072 00114 (main.go:13)       RET0x0073 00115 (main.go:13)       CALL    runtime.deferreturn(SB)0x0078 00120 (main.go:13)       MOVQ    56(SP), BP0x007d 00125 (main.go:13)       ADDQ    $64, SP0x0081 00129 (main.go:13)       RET...

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/88495.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/88495.shtml
英文地址,請注明出處:http://en.pswp.cn/web/88495.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Android 12系統源碼_分屏模式(一)從最近任務觸發分屏模式

前言 打開MainActivity&#xff0c;然后進入最近任務觸發分屏&#xff0c;可以成功進入分屏模式。 本篇文章我們來具體梳理一下這個過程的源碼調用流程。 一 launcher3階段 1.1 源碼 //packages/apps/Launcher3/quickstep/src/com/android/quickstep/views/TaskView.java publi…

Flask 入門教程:用 Python 快速搭建你的第一個 Web 應用

文章目錄前言一、什么是 Flask&#xff1f;&#x1f4cc; Flask 的優勢1. 輕量靈活2. 易于上手3. 可擴展性強4. 自由度高5. 社區活躍&#xff0c;資料豐富Flask 主要用來做什么&#xff1f;二、Flask快速入門1.創建一個Flask項目2.開啟debug&#xff0c;修改host&#xff0c;端…

實習第一個小需求樣式問題總結

Vue2 vxe-table Element UI 表頭下拉詳情實現總結一、核心功能實現表頭下拉按鈕交互初始嘗試 expand-change 事件無法滿足需求&#xff0c;改用 vxe-table 的 toggle-row-expand 事件&#xff1a;<vxe-table toggle-row-expand"handleExpandChange"><temp…

Linux中LVM邏輯卷擴容

在Linux系統中對根目錄所在的LVM邏輯卷進行擴容&#xff0c;需要依次完成 物理卷擴容 ? 卷組擴容 ? 邏輯卷擴容 ? 文件系統擴容 四個步驟。以下是詳細操作流程&#xff1a;一、確認當前磁盤和LVM狀態# 1. 查看磁盤空間使用情況 df -h /# 2. 查看塊設備及LVM層級關系 lsblk# …

微軟365 PDF導出功能存在本地文件包含漏洞,可泄露敏感服務器數據

微軟365的"導出為PDF"功能近期被發現存在嚴重的本地文件包含(Local File Inclusion, LFI)漏洞&#xff0c;攻擊者可利用該漏洞獲取服務器端的敏感數據&#xff0c;包括配置文件、數據庫憑證和應用程序源代碼。該漏洞由安全研究員Gianluca Baldi發現并報告給微軟&…

臺球 PCOL:極致物理還原的網頁斯諾克引擎(附源碼深度解析)

> 無需下載,打開瀏覽器即可體驗專業級斯諾克!本文將揭秘網頁版臺球游戲的物理引擎與渲染核心技術 在游戲開發領域,臺球物理模擬一直被視為**剛體動力學皇冠上的明珠**。今天我們要解析的**臺球 PCOL**(Pure Canvas Online Billiards)正是一款突破性的網頁版斯諾克游戲…

springboot-2.3.3.RELEASE升級2.7.16,swagger2.9.2升級3.0.0過程

一、pom文件版本修改<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.16</version><relativePath/> </parent>如果用到了“spring-boot-starter…

Python-正則表達式-信息提取-滑動窗口-數據分發-文件加載及分析器-瀏覽器分析-學習筆記

序 欠4前年的一份筆記 &#xff0c;獻給今后的自己。 正則表達式 概述 正則表達式&#xff0c;Regular Expression&#xff0c;縮寫為regex、regexp、RE等。 正則表達式是文本處理極為重要的技術&#xff0c;用它可以對字符串按照某種規則進行檢索、替換。 1970年代&…

一文入門神經網絡:神經網絡概念初識

神經網絡的世界遠比你想象得更豐富多元。從基礎架構到前沿融合模型&#xff0c;我為你梳理了當前最值得關注的神經網絡類型&#xff0c;不僅包括那些“教科書級”的經典模型&#xff0c;也覆蓋了正在改變行業格局的新興架構。以下是系統分類與核心特點總結&#xff1a;一、基礎…

線上事故處理記錄

線上事故處理記錄 一、MySQL 導致的服務器 CPU 飆升 有一天&#xff0c;突然收到了服務器 CPU 飆升的告警信息&#xff0c;打開普羅米修斯查看 CPU 的使用情況&#xff0c;發現 CPU 確實飆升了&#xff0c;下面開始去進行問題定位了。 1. 首先連接到對應的服務器&#xff0c;然…

ParaCAD 筆記 png 圖紙標注數據集

ParaCAD-Dataset git lfs install git clone https://www.modelscope.cn/datasets/yuwenbonnie/ParaCAD-Dataset.git https://github.com/ParaCAD/ 不止100g 下個最小的 沒有三視圖

C#使用Semantic Kernel實現Embedding功能

1、背景 C#開發中&#xff0c;可以通過Semantic Kernel實現本地模型的調用和實現。 本地的Ollama的版本如下&#xff1a;安裝的Package如下&#xff1a;2、代碼實現 // See https://aka.ms/new-console-template for more information using Microsoft.Extensions.AI; using Mi…

轉轉APP逆向

APP版本 11.15.0 接口分析 # URL https://app.zhuanzhuan.com/zz/transfer/search# header cookie xxx x-zz-monitoring-metrics feMetricAntiCheatLevelV1 zztk user-agent Zhuan/11.15.0 (11015000) Dalvik/2.1.0 (Linux; U; Android 10; Pixel 3 Build/QQ3A.200805.001) z…

注解與反射的完美配合:Java中的聲明式編程實踐

注解與反射的完美配合&#xff1a;Java中的聲明式編程實踐 目錄 引言 核心概念 工作機制 實戰示例 傳統方式的痛點 注解反射的優勢 實際應用場景 最佳實踐 總結 引言 在現代Java開發中&#xff0c;我們經常看到這樣的代碼&#xff1a; Range(min 1, max 50)priva…

開源入侵防御系統——CrowdSec

1、簡介 CrowdSec 是一款現代化、開源、基于行為的入侵防御系統&#xff08;IDS/IPS&#xff09;&#xff0c;專為保護服務器、服務、容器、云原生應用而設計。它通過分析日志檢測可疑行為&#xff0c;并可基于社區協作共享惡意 IP 黑名單&#xff0c;從而實現分布式防御。 其…

imx6ull-裸機學習實驗13——串口格式化函數移植實驗

目錄 前言 格式化函數 實驗程序編寫 stdio文件夾 main.c Makefile修改 編譯下載 前言 在學習實驗12&#xff1a;imx6ull串口通信實驗&#xff0c;我們實現了 UART1 基本的數據收發功能&#xff0c;雖然可以用來調試程序&#xff0c;但是功能太單一了&#xff0c;只能輸出…

CCF-GESP 等級考試 2025年6月認證C++三級真題解析

1 單選題&#xff08;每題 2 分&#xff0c;共 30 分&#xff09;第1題 8位二進制原碼能表示的最小整數是&#xff1a;&#xff08; &#xff09;A. -127 B. -128 C. -255 …

【網絡安全】服務間身份認證與授權模式

未經許可,不得轉載。 文章目錄 問題背景用戶到服務的身份認證與授權系統對系統的通信服務與服務之間的通信需求分析Basic Auth(基本身份認證)優點缺點mTLS 證書認證優點缺點OAuth 2.0優點缺點JWS(JSON Web Signature)優點缺點結合 Open Policy Agent 的 JWS 方案優點缺點結…

【EGSR2025】材質+擴散模型+神經網絡相關論文整理隨筆(四)

An evaluation of SVBRDF Prediction from Generative Image Models for Appearance Modeling of 3D Scenes輸入3D場景的幾何和一張參考圖像&#xff0c;通過擴散模型和SVBRDF預測器獲取多視角的材質maps&#xff0c;這些maps最終合并成場景的紋理地圖集&#xff0c;并支持在任…

Grid網格布局完整功能介紹和示例演示

CSS Grid布局是一種強大的二維布局系統&#xff0c;可以將頁面劃分為行和列&#xff0c;精確控制元素的位置和大小。以下是其完整功能介紹和示例演示&#xff1a; 基本概念 網格容器&#xff08;Grid Container&#xff09;&#xff1a;應用display: grid的元素。網格項&#x…