深入理解 Go 語言的棧

本期深入研究 Go 堆棧的工作原理以及作爲程序員爲什麼要關心它。

本篇內容是根據 2023 年 3 月份#288 A deep dive into Go's stack[1] 音頻錄製內容的整理與翻譯

過程中爲符合中文慣用表達有適當刪改, 版權歸原作者所有.

Mat Ryer: 大家好,歡迎收聽 Go Time。我是 Mat Ryer[2]。今天我們要討論的是 Go 的棧。究竟什麼是棧?它的作用是什麼?我們作爲程序員需要多關注它?要有效編寫程序,我們需要了解多少?今天我們將深入探討這些問題。今天和我一起主持的是 Kris Brandow[3]。你好,Kris。

Kris Brandow: 你好,Mat,你好嗎?

Mat Ryer: 還不錯。最近你的大樓裏有什麼戲劇性的事情發生嗎?

Kris Brandow: 嗯,不幸的是有。最近紐約市發生了一些不幸的事故,而我也受到了影響。不過現在一切都好了,我們都沒事了。

Mat Ryer: 好的,太好了。但你沒有導致這些事故吧?

Kris Brandow: 沒有。

Mat Ryer: 嗯,那就好。今天我們還請到了 Yarden Laifenfeld[4]。你好,Yarden。Yarden 是 Rookout[5] 的軟件工程師,或者說從週一開始是 Dynatrace[6] 的軟件工程師,你在那兒開發了一個用於 Go 的生產級調試器。這真是非常激動人心的項目,我們肯定會討論到這個。另外,你也是 GopherCon Israel[7] 的組織者之一,同時也是 Women Who Go Israel 小組的一員。歡迎你,Yarden。

Yarden Laifenfeld: 你好,謝謝你邀請我。

Mat Ryer: 非常榮幸。而且我們還請到了來自 Go 團隊的 David Chase[8]。David 已經在編譯器和運行時方面工作了大約 40 年,並且自 2015 年起在 Google 開始爲 Go 工作。我想那已經是 8 年前的事了,David。歡迎來到 Go Time。

David Chase: 你好。我不知道該說什麼。

Mat Ryer: 不,你好就夠了,完全沒問題。不過你有個非常有趣的個人簡歷,你種植百合花,而且你還是北美百合花協會的評委。

David Chase: 那是很久以前的事了…… 我們以前去度假---那時候我們有工作,有假期,但他們讓我開車。我太太一個人計劃了一些日子,而我只提前五秒鐘計劃。

Mat Ryer: 就是即時規劃。

David Chase: 是的,完全是這樣。於是我們開車時看到了一個標誌,上面寫着 “下一路口有百合花和秋海棠”。我們想“哦,挺酷的,我們喜歡百合花。” 於是我們就去了。結果我們遇到的那個人是一個非常有名且極具創新性的百合花育種專家。我們覺得 “哦,真是太酷了。” 他給了我他的目錄,我訂購了一些百合花開始種植…… 就這樣,我一直種了很多年…… 我很喜歡它們,本地有一個協會,他們說 “哦,我們需要一些幫手,需要更多的幫手,還需要評委……” 我經過了一個多年的培訓過程,包括考試和實地評審,最終成爲了評委。我必須強調,我只是一個初級評委。你會驚歎於你學到的東西,但同時那些真正厲害的人讓你覺得 “天哪,你真的能做得這麼好。” 這有點奇怪。有時你和別人一起出去,他們會問你關於百合花的問題,你就會開始信口開河地解釋這個那個。“哦天哪,我竟然說了這麼多。”然後你會覺得“好吧,挺酷的。”

Mat Ryer: 真是太棒了。百合花難種嗎?

David Chase: 並不特別難,這也是我喜歡它們的原因之一。我可以集中精力做些重要的工作,然後我就可以把它們放在那裏,自然生長。在美國,有一種從歐洲引入的害蟲,曾經讓種植百合花變得非常困難。但後來引入了一種特定的黃蜂,專門捕食這種害蟲的幼蟲,現在這種黃蜂已經在這裏定居了,這讓種植變得很容易。你只要種下它們,它們就會長得很好。

Mat Ryer:  太神奇了。

David Chase: 我該停下來了,我可以再說很久。

Mat Ryer: 不不不,也許我們應該開一個 “百合時間” 節目。[笑聲]

David Chase: 好,我們可以做這個。

Mat Ryer: 對,下次我們就做這個。哇,真是太棒了。Yarden---其實你和 David 有一個共同點,你們都喜歡自行車,喜歡騎車。Yarden,你經常騎車嗎?

Yarden Laifenfeld: 我寫這個是迴應 David 提到的自行車。

Mat Ryer: 嗯,我又讀了一遍。你說你不擅長騎車。

Yarden Laifenfeld: [笑] 是的,這是真的。我不會說我不擅長……

Mat Ryer: 但你確實這麼說了。

Yarden Laifenfeld: …… 不過我確實不擅長。[笑] 我並不特別擅長騎車。我會騎,也確實騎車。

Mat Ryer: 你還能要求什麼呢?

Yarden Laifenfeld: 完全正確。

Mat Ryer: 是的,不過修車可能也挺重要的。

David Chase: 修車也是。

Yarden Laifenfeld: 有人幫我修。

Mat Ryer: 對,可以讓 David 幫你修。David 也會修自行車。如果你想種百合花或者需要修自行車,David 是你 (要找) 的人。

David Chase: 也許吧。

Mat Ryer: Yarden,在你開始使用 Go 之前,你寫過 Java、Ruby、C#、C++、Python…… 是什麼讓 Go 吸引了你的注意?

Yarden Laifenfeld: 實際上我現在還在用這些語言。在 Rookout---或者我應該說 Dynatrace---我們現在支持所有這些語言。但我的主要工作還是 Go。我在使用 Go 之前寫過 C,我非常喜歡它---它的簡潔性以及接近底層硬件的特性。我知道它實際上並沒有那麼接近底層,但它已經是現代人們能接觸到的最底層的了……

Mat Ryer: 對,相對來說是這樣的。

Yarden Laifenfeld: 是的,完全正確。所以我更喜歡 Go 的那種簡潔性,而不是那些更高級的語言。

Mat Ryer: 那麼,在這些語言中,你最喜歡哪一個呢?

Yarden Laifenfeld: 肯定是 Go 或者 C。

Mat Ryer: 很不錯的回答。好的,那我們開始吧…… 我想從基礎開始。什麼是棧?它到底是什麼,它的作用是什麼?

David Chase:  在 Go 中,棧有點像一個內部的 slice(切片)。它的工作方式非常類似。棧有一個容量,它是從高地址分配到低地址,而不是像 slice 那樣從低到高。而每當你調用一個函數或方法時,棧就會向低地址擴展一個固定的量。每個函數有它自己的固定棧幀大小。棧用於臨時存儲局部變量以及需要溢出到內存中的臨時變量---這些變量會被存儲在棧中。根據你的架構的調用約定,你可能會在棧空間中傳遞給你調用的函數和方法參數。從 Go 1.17 開始,在某些架構上我們會使用寄存器來傳遞參數…… 但我們仍然會保留棧空間來存儲這些寄存器的溢出值。

棧與 slice 的不同之處在於---其實,這不完全對。slice 也有容量,但你可以不斷地追加數據。如果你追加的數據超出了容量,slice 會重新分配一個更大的 slice。而在棧的情況下,Go 不同於很多編程語言。它會分配一個新的棧,並將舊的棧複製到新的棧中。

Go 的一個特別之處在於,它會記錄棧上所有可能存在的指針位置。當你複製棧時,這些指針都會被更新。因此,你的程序完全不知道這一過程的發生。你只是調用了一個函數,棧複製到了一個新的位置,變得更大了,所有指向棧的內部指針都在複製過程中更新了,然後程序繼續運行。

Mat Ryer: 這是一個昂貴的操作嗎?

David Chase: 它確實有點昂貴…… 但實際上它只是內存複製,然後掃描棧並解釋指針。不過你不會經常這樣做。它有一種滯後效應。棧會保持較大的狀態,直到垃圾回收器檢查一個 goroutine 的棧時發現 “哇,我們分配了你 1 MB 的棧空間,但你只用了 10 KB。我覺得我們應該收回一些空間。” 然後它會修復棧並把你放回到一個較小的棧中。所以增長棧的操作是昂貴的,但你不會經常遇到這種情況,除非棧增長到很大。

一種替代實現是分段棧,這種方法以前在其他編程語言中也有使用過。在這種方式下,你不會重新定位舊的棧,而是分配一個新的棧段。但是這種方式有一個滯後問題和交叉問題。如果你恰好在調用很多函數的地方遇到邊界,你總是會碰到它,因爲它不是一個平滑的增量,而是 “哦,我撞到了盡頭,需要做一些額外的操作。” 即使你已經預留了下一個棧段並打算重用它,你仍然會碰到這個邊界。這種操作非常昂貴,所有我知道使用過這種分段方法的人,除非他們有很好的理由繼續使用這種方式---而 Go 不需要這些理由---他們最終都會轉向連續的棧並進行復制。

Kris Brandow: 當垃圾回收器縮小棧時,是在 goroutine 完成任務之後嗎?還是說會暫停 goroutine?這個操作具體是在什麼時候進行的?因爲你提到我們在函數調用時擴展棧,這是一個乾淨的時刻。那麼棧縮小時也會有類似的乾淨時刻嗎?

David Chase: 當然了,如果你的 goroutine 暫停了,也就是它沒有在實際運行。它可能處於可運行狀態,但沒有被分配到線程上實際運行。在這種情況下,我相信如果它處於一個乾淨的停頓點,棧就可能會被縮小。這裏我們得談一下搶佔。如果你要搶佔一個 goroutine---抱歉,應該說如果你需要搶佔一個 goroutine,比如你需要進行垃圾回收,那麼垃圾回收器需要與線程進行握手。這裏有兩種握手方式:合作式和非合作式。合作式搶佔會發生在幾乎每個函數的入口序列中,它會檢查棧是否溢出。而當運行時需要中斷時,它會謊報棧邊界,goroutine 會說:“哦,看來我需要一個新的棧。” 然後它進入該代碼,發現實際上它是因爲其他原因被中斷,某些人需要與它交互。這就是一個乾淨的合作式搶佔,所有指針的狀態都是已知的,這時你就可以選擇縮小棧。因爲如果需要增長棧,也是通過這種方式,所以這是合理的。如果你不能找到指針,你也無法增長棧。

非合作式搶佔發生在你有一個長時間運行的緊密循環時。我們不會在循環的回邊上進行檢查。我們會評估是否應該在這裏顯式檢查,但我們沒能將其成本降低到足夠低。所以我們做的是中斷 goroutine,並在某些地方無法中斷時記錄這些地方,檢查並判斷 “我是否處於一個足夠安全的地方?” 這不是一個你知道一切的安全點,但它是一個足夠安全的地方。你可能不知道所有的細節,但你知道在那個點上進行垃圾回收是可以的,檢查棧也是可以的,但你並不知道最後一個棧幀中的所有指針信息,所以你不能移動棧。抱歉說了這麼多,但這是有原因的,並且比八年前複雜了很多。

Mat Ryer: 聽起來絕對不簡單。你提到的一個有趣的點,Yarden,也許你可以對此做些解釋…… 你提到棧會增長到較低的內存地址。這是怎麼回事?它是怎麼做到的?它真的只是預留了一堆內存,然後向後工作嗎?這樣做的好處是什麼?

Yarden Laifenfeld: 我首先要說的是,Go 的棧運作方式與普通棧非常相似。我的意思是,即使你用 C 寫一個二進制文件,其中沒有運行時去控制棧和類似的東西,你依然會有一塊內存區域叫做棧,它的工作方式非常類似,其中很多東西也是類似的。所以 Go 真正模擬了這種行爲,這很有道理,因爲這是一個好概念。

在一個普通的---我說普通的意思是非 Go 或非託管的二進制文件中,棧確實是從高地址向低地址增長的…… 因爲你有一大塊內存,或者你有一大段內存,其中有不同的部分,棧是其中的一部分,堆是另一部分,它們會相互接近。所以堆從低地址向高地址增長,棧從高地址向低地址增長,它們會越來越接近。

所以我說 Go 也是這樣做的…… 關於分配的問題是---David,如果我錯了請糾正我。通常當需要時它會分配一個棧,但如果我記得沒錯的話,它通常會把棧保留起來。已經分配的棧會有很多重用的情況。

David Chase: 我不認爲它有意圖地重用棧。但可能會發生這種情況。這是運行時中我不太瞭解的部分。垃圾回收器會傾向於重用較小的棧,因爲它會認爲 “這是一個大小類別”。因此任何這個大小的東西都會進入一個 4k、8k 或 16k 的小塊中,然後下次需要相同大小的東西時,它就會再次使用這些小塊。

Yarden Laifenfeld: 哦,那很酷。我以爲這是專門爲棧設計的,但我猜這適用於所有小塊。

David Chase: 可能是。這是我不太瞭解的一個部分。

Kris Brandow: 聽起來棧就像是某種特殊內存空間中的切片,它們專門用於 goroutine。這聽起來棧沒有那麼特別。

David Chase: 有點像。棧的末尾有一些額外的信息,比如 “嗯,我們把 G 放在哪裏了?” 每個 goroutine 都有一個叫做 G 的東西,或者叫 G 結構。我其實不知道我們是把它放在棧的底部還是末端。

Mat Ryer: G 代表什麼?

David Chase: 我想是 goroutine。

Mat Ryer: 很酷的名字,是個好名字。

Kris Brandow: [笑]

Mat Ryer: 嗯,如果我看代碼時看到一個叫 G 的結構體…… 顯然,它是那個領域中的一個概念。

Kris Brandow: 也有一些其他的單字母東西,對吧?比如 G、P 和 M…

David Chase: 是的……

Yarden Laifenfeld: 問題是你怎麼給你的變量類型命名爲 G?

Mat Ryer: 小寫 g?

Yarden Laifenfeld: 你會叫它 g 嗎?

Mat Ryer: 也許讀作 Gee?

David Chase: 不,就是 G。

Mat Ryer: 就是 g。

David Chase: 就是 g。

Mat Ryer: 是的。但這些是主要的概念嗎?因此它們應該被賦予這樣的地位?因爲我覺得這有點像是權力的象徵---一個結構體僅用一個字母命名,聽起來有點霸氣。是不是有點像在炫耀?

David Chase: 好吧,這是在運行時中的,所以有它自己的命名空間。它不會污染其他人的命名空間。你可以有你自己的 g,你可以有你自己的 m 和 p。這是允許的。

Mat Ryer: 是的。但問題是,你應該這樣做嗎?這纔是問題所在,不是嗎,David?

Yarden Laifenfeld: 我認爲這些是基礎概念。我學習 Go 運行時的方式基本上是通過閱讀代碼,試圖理解發生了什麼。你們在維護可讀代碼方面做得相當不錯,但有些地方仍然很難理解,尤其是當我不知道爲什麼要這樣做時。所以我通常會通過閱讀那些編寫代碼的人寫的東西來補充我的理解。所以當你開始深入運行時的代碼時,你會發現很多地方會談到 p、m 和 g,因爲這是 Go 如何運行、如何如此高效以及如何實現 goroutines 或輕量級線程的基礎。這一切都是從這裏開始的。

Mat Ryer: 是的。當然,Go 是開源的,這一點非常棒。我們可以去看這些代碼,實際上深入研究這些代碼。而且這些代碼是用 Go 寫的。聽起來這不是最容易理解的代碼庫…… 但我們有機會理解它。不過 Yarden,你是否經常需要深入研究這些代碼,鑑於你所從事的工作?

Yarden Laifenfeld: 我確實需要經常研究這些代碼。

Mat Ryer: 哦,你很喜歡這樣做。

Yarden Laifenfeld: 是的,這非常有趣,因爲它真的非常複雜,而且其中的實現方式非常驚人。因爲我也在寫 Go 代碼,所以通過閱讀那些運行我代碼或編譯我代碼的代碼,我就能更好地理解我的代碼發生了什麼。理解的層次非常多,這讓我成爲了一個更好的開發者,也讓我覺得很有趣。

Mat Ryer: 那麼你會建議人們爲了這個原因去深入學習這些代碼嗎?還是說你可以成爲一個足夠好的 Go 程序員,而不用瞭解這些東西,直接讓 David 他們去操心這些事?

Yarden Laifenfeld: 我認爲這因人而異。如果你剛開始接觸這門語言,那麼深入研究它的內部機制或如何運行並不是正確的方式。但我確實認爲,如果你已經使用 Go 一段時間了,或者它是你工作中的一個重要部分,那麼深入瞭解它可能會讓你成爲一個更好的開發者。因爲它不僅可以幫助你理解一些事情,還能幫助你避免因爲使用不當而產生的一些常見但仍然是錯誤的使用方式。

從積極的方面來說,它還可以讓你的代碼變得更好,因爲你可以通過了解運行時的一些小細節以及運行時的工作原理來提高性能。如果你瞭解不同的內存區域,你可以控制哪些東西放在哪裏,諸如此類的事情。所以,對於更高級的開發者來說,我認爲這很重要。

Kris Brandow: 我認爲了解一些歷史知識---在過去,也許這會更有用。比如我們談到這些 g、m 和 p……m 代表線程,我不知道爲什麼不叫它 t,但無所謂……p 實際上是處理器上的核心。在 Go 1.5 之前,默認情況下你只有一個 p。所以即使你有很多 goroutines 在運行,你實際上還是一次只做一件事;你並沒有進行並行執行。我記得在我剛學 Go 時,這一點讓我很驚訝。我以爲自己同時在做很多事情,但實際上它們只是併發運行,而不是並行運行。我想當你開始研究運行時的東西時,你會看到這些差異在哪裏,因爲這些概念真的很難理解。我記得第一次看 Rob Pike 的演講《併發不是並行》時,我覺得 “這真的很難理解。” 但當你能親眼看到這些東西時,我覺得這會有很大幫助。

Yarden Laifenfeld: 是的,我完全同意。而且我覺得這讓它更貼近我們開發者的層面。當我想到 Go 的開發者時,他們在我心裏是那種掌握了一切知識的虛幻生物,知道如何創建完美的編程語言---

Mat Ryer: 你看過我的代碼嗎?[笑聲] 哦,你是說 Go 團隊。

Yarden Laifenfeld: 是的,不是個人開發者。我說的是 Go 團隊。

Mat Ryer: 嗯,明白了。

Yarden Laifenfeld: 不過說真的,我會想,“這些人知道一切,知道所有的工作原理……” 然後我閱讀代碼時發現,“哦,等等,他們的代碼和我寫的差不多。” 我寫代碼,他們也寫代碼,我可以讀懂他們寫的代碼。突然之間,從社區的角度來看,我覺得這很酷,因爲他們也是社區的一部分,我們都是其中的一員。

Mat Ryer: 是的,我能理解這種感覺確實存在。不過,Yarden,別忘了你非常聰明。並不是我們所有人都如此。

Yarden Laifenfeld: 哦,抱歉,Mat。

Mat Ryer: 不,不是我…… 我是在說 Kris。

Kris Brandow: 是在說我?!

Mat Ryer: 我只是開玩笑。當然不是。

David Chase: 說的是大家。

Mat Ryer: 是的…… 其實,我記得 George Carlin[9] 曾說過,“想象一下一個普通聰明的人,然後意識到有一半的人比這個還笨……” 這雖然有點殘酷,但也挺有趣的。 (譯者注: George Carlin 是美國喜劇演員, 社會活動家, 批評家)

David Chase: 我覺得這有點刻薄……

Mat Ryer: 是的,有點。

David Chase:另一種看法是你很忙,有很多事情要做,所以有些事情會被你忘記。

Mat Ryer:完全沒錯。這就是爲什麼我的襪子穿反了。我太忙了。

David Chase:是啊,你有比襪子方向更重要的事情要處理。

Mat Ryer:褲子。

David Chase:褲子。

Kris Brandow:我覺得我們大家---這有點哲學意味,但我認爲我們每個人都在不同的方面聰明。我覺得這就是你在說的,Yarden,當你看到 Go 團隊時。從外面看,感覺是 “哇,他們對一切都瞭如指掌。” 但其實不是的,有些人對編譯器非常熟悉,有些人對運行時非常瞭解,他們對某些部分非常在行,但對其他部分一無所知…… 他們依賴團隊的其他成員來填補信息。

Yarden Laifenfeld: 這其實讓人有點安慰。

Mat Ryer:是的,絕對是這樣。那麼,David,當你加入 Go 團隊時,你之前寫 Go 寫了多久?

David Chase:哦,差不多是零天。

Mat Ryer:那你一定是通過了非常厲害的面試。

David Chase:有個規定,當時有人告訴我,我們可以公開談論這個…… 面試的規則是---怎麼禮貌地說呢?不要在面試中胡扯。

Mat Ryer:對,這是普遍的好建議。

David Chase:堅持你所知道的內容,這就是你被評估的內容。你不會因爲你不知道的東西被評估。評估的是你在你擅長的事情上有多好。所以我當時不知道任何 Go,但這沒關係……

Mat Ryer:你帶了一朵百合花去嗎?

David Chase:不是爲了面試。我確實帶過幾次…… 你在花園裏有一朵漂亮的花,就把它帶進來,感覺很好。

Mat Ryer:那真的很不錯。

David Chase:是的,所以我那時候是零天。我開始學習 Go---這不是一門難學的語言。我遇到了一些小麻煩---我在想,怎麼向一個真正的初學者解釋這個呢?有些類型有點像引用類型,比如切片和映射。映射實際上是引用類型。如果你傳遞一個映射並修改它,你會看到這些修改。如果你傳遞一個切片並修改它,你會看到部分修改;但如果它增長了,你就看不到了。你只能看到你傳遞的那部分數據。對我來說,這有點毛骨悚然…… 你看不到擴展部分,你只能看到對傳遞部分的更改。

Mat Ryer:嗯,這確實有點不尋常。

David Chase是的,這是語言中一直讓我覺得有點奇怪的部分…… 但當你看現實生活中的問題時,似乎從來沒有人搞錯過。我不太明白。也許人們只是---怎麼說呢…… 我總是有點擔心可能出現的病態問題是什麼?所以我立刻想到 “哦,他們可能會這樣做,這樣就會出問題。” 但實際上人們並不會這麼做,所以問題不大。

Yarden Laifenfeld:也許人們在第一次嘗試時受過傷害,然後他們找到了一種可行的方式,並一直堅持使用它。

David Chase:我甚至不記得聽說過有人因此受傷。這就是奇怪的地方。我有一個朋友,他以一種非常不同的方式在使用 Go…… 他有很深的編程語言背景,曾是助理教授,曾在 IBM 研究院工作…… 當你向他解釋這些事情時,他會說:“這太糟糕了。但它從來沒有成爲問題……”[笑聲] 所以這有點奇怪,沒有人似乎搞錯過。

所以是的,我很快就學會了這門語言…… 這很好,因爲我正在爲它編寫一個編譯器,所以我確實需要知道它是如何工作的。

Mat Ryer:是的,但正如 Yarden 所說,這突顯了一個重要的教訓,那就是:你需要擅長學習如何學習。這纔是重要的技能。你不需要知道所有的事情,也不需要把所有事情都記在腦子裏。你需要能夠有針對性地學習,基於你正在處理的事情,或者你正在解決的問題…… 因爲這也是很多初級開發人員的一個問題---正如 Yarden 所說,他們看到有人在做演講,演講內容非常豐富。顯然,他們要麼做了大量的研究,要麼有直接的經驗。最好的演講通常是有人在講述他們真實經歷過的事情。所以他們爲了做這個演講,專門研究了這個領域。這也是一個給演講的好理由…… 如果你真的關心並想學習某些東西,準備一個演講是一個很好的方式。但你不需要知道所有的事情,並把所有的東西都記在腦子裏。我覺得這對每個人來說都是一個很好的提醒,特別是當你是新手時,因爲你沒有太多的經驗來處理這些事情,這看起來別人就像天才一樣。

Yarden Laifenfeld:是的,我有很多輔導非常非常初學者的經驗。而我遇到的最大問題是,他們不敢嘗試他們想到的解決方案…… 他們會坐在電腦前,看着屏幕。我會過去問:“怎麼了?” 他們會說:“我不確定該怎麼做。” 我會問:“那你是怎麼想的?” 他們會告訴我解決方案,可能是對的,也可能是錯的,但我會說:“好,那你爲什麼不寫下來試試呢?” 他們沒有真正的理由,只是說:“哦,我不知道我可以那樣做。” 或者 “我不知道那些東西在那裏。” 我覺得這是成爲更好開發者的第一步,就是通過嘗試和學習來進步。

David Chase:絕對是 “哦,我不知道,真的嗎?我可以那樣做?好的……”

David Chase:即使是 Go 內部代碼也是這樣。 [笑]

David Chase:是的,有些部分有點嚇人…… 併發的東西就是這樣---或者說是那些奇怪的調優,比如 “我需要與那邊的線程同步…… 我覺得我會稍微自旋一下。” 有人發現,“是的,這是個好主意。” 但有時候如果出現了某種奇怪的架構變化,自旋就不合適了,比如你的處理器的緩存出問題了,或者你自旋的那個東西沒有進入你的緩存,導致你有個大問題。

Kris Brandow:我覺得 Go 很擅長讓你仍然可以接觸到那些可怕的東西,但不會一開始就把它們拋給你。我覺得你說到切片,David,我覺得人們不會搞砸切片的原因之一是,他們很晚才學到數組。在你學語言的初期,數組並不是經常出現的東西。通常是 “這是個切片,在其他語言中你會有這樣的東西,叫做數組。把它當成那樣用。” 我覺得如果你一開始就告訴他們 “好吧,有這些切片,還有這些數組。它們很相似,但不一樣。一個可以增長,另一個不能。” 這可能會讓切片更加令人困惑。但只是告訴你 “這是一個東西,是一組數據項。就這樣用它。” 人們通常會覺得:“好吧,我就這樣用。”

David Chase:你可以在末尾添加新的數據項,一直這樣做,沒問題。

Kris Brandow:是的,我同意這有趣的地方在於,人們能夠直觀地理解 “哦,如果我修改已有的內容,我會在兩邊都看到修改;但如果我擴容了這個東西,那就是一個新的東西。” 但我覺得 API 在這方面也很有幫助。比如 “哦,如果我 append,我會得到一個新的切片,這與我之前的切片是不同的。之前的切片保持不變。”

Mat Ryer:是,這是真的。不過,append 給切片是有點不尋常的。在其他語言中我很少見到這種情況。特別是,我在說 Ruby 和 C#,我想也是這樣的。有時候我也見過有人 append 了數據,但沒有重新賦值給切片,結果當然就出了問題。但正如你所說,David,這種情況其實非常罕見。

David Chase:可能只是因爲它們一開始解釋得很好。

Mat Ryer:是的,你學會了如何 append,所以你就這樣做了,沒問題。

David Chase:是的。

Mat Ryer:Yarden,我想回到你剛纔說的…… 你提到了解這些內部機制讓你成爲了一個更好的程序員。我們如何控制什麼放在棧上,什麼放在堆上?因爲在任何時候你都不會說 “哦,放到棧上。” 你不會調用某個函數來實現這個。那麼我們怎麼知道東西會放在棧上還是堆上?我們如何控制它?

Yarden Laifenfeld: 這是個好問題。我想說 Go 裏有一些神祕的黑魔法,我並不完全瞭解。我可以告訴你哪些肯定會放在棧上,比如當你創建一個局部變量時,它會放在棧上。或者當你把一個參數傳遞給函數時,它可能會放在棧上。David 剛纔提到它可能會在寄存器裏,但我覺得總體上來說,它不會放在堆上。所以我們應該這樣想。

然後事情開始變得複雜的是,哪些東西不放在棧上,而是放在堆上。這通常是那些我們事先不知道會佔用多少內存的東西。所以,如果我們想象一個普通的變量,比如整數或者浮點數,我們提前就知道它會佔用多少內存,所以它會放在棧上。但如果我們創建一個映射、切片或者一個我們不知道有多少個元素的數組,我猜這些東西會放在堆上。我剛纔說有一些魔法在起作用,這取決於你具體是怎麼做的,但總體上是這個思路。

當我們談到指針時,事情會變得有點複雜。如果我們把一個指針作爲參數傳遞給函數---有趣的是,垃圾回收器如何知道這個指針什麼時候不再使用,或者它指向的數據什麼時候可以釋放?總體來說,思路是儘量把東西放在棧上,因爲正如 David 所說,棧是臨時存儲。它會自動清理,不需要垃圾回收器。而且只有在你確實需要使用指針,或者確實需要在堆上存儲東西時,你纔會這麼做,以此避免垃圾回收器運行的開銷。

Mat Ryer:對。你說當東西在棧上時,它會自動清理…… 那麼函數返回時會發生什麼?因爲假設這些參數會在調用函數時放在棧上。那麼當這個函數返回時,會發生什麼呢?

Yarden Laifenfeld:是的,理論上是會發生一些事情。當我們談論棧時,我們通常會想到棧的末端有個指針,然後當我們從函數返回時,這個指針會移動…… 所以棧的結構是從高地址向低地址增長的,最後一塊是最後調用的函數。如果我們在某個時刻看棧追蹤,內存就是這樣排列的:棧上會有最後一個函數的變量,然後是調用它的那個函數的變量,依次類推。

我們可以想象棧末端的指針在當前函數返回時移動到前一個函數的位置。這是理論上我們做了一些事情的地方。然而,實際上,除了那個指針,其他什麼都沒變。內存沒有消失,它也沒有被清零,下次我們寫入相同的棧空間時,它只會被覆蓋,我們基本上會認爲它不存在了。所以棧並沒有真正地增長和縮小,只是它的末端指針在移動。

Mat Ryer:是啊。我猜清零內存或者做其他操作會是額外的工作,所以沒必要這樣做,對吧?

Yarden Laifenfeld:對,沒錯。做最少的工作去實現你想要的東西。

David Chase:  人們不想要它。那樣會變慢,而人們不喜歡變慢。加密領域的人曾經提出過類似的需求---如果我在內存中寫入了重要信息,我能多快將它清零?他們曾經問過這個問題,並且也有人提出過解決方案。我們還不知道什麼是最好的方法。

Mat Ryer: 他們不能通過編程方式將它改爲另一個值嗎?

David Chase: 問題在於,編譯器看到你對某些內容進行了許多寫入操作,但實際上你並沒有讀取或使用這些內容,它會說:“我可以讓這個過程更快。讓我把這些寫入操作去掉。” 這也告訴你正確的做法是,你需要添加另一段代碼來驗證你寫入的內容確實被清零了。但這會花費更多時間。而加密領域的那些人會說:“等等,我們不希望花這麼多時間。”

Mat Ryer: 是的…… 這樣真的沒辦法讓他們滿意,對吧?

Yarden Laifenfeld: 是啊,總是那些加密領域的人……

David Chase: 沒法讓任何人滿意。

Kris Brandow: 說到安全性---那真的很慢…… 我聽過《Changelog & Friends》[10] 的一期節目,裏面有 Oxide Computer[11] 的人,他們說他們需要把打印出密鑰的打印機拿出來,並在其微控制器上鑽一個洞,以確保密鑰永遠不會被恢復。就安全而言,這是一整套複雜的過程。但這也說明了我們是如何看待計算的,大家總是在追求 “速度、速度、速度”,而有時候我們可能應該在安全性和速度之間做一些權衡。我們只需要找到那個平衡點。

Mat Ryer: Kris,你覺得多快能把一臺打印機砸壞?我覺得我能很快砸壞一臺打印機,完全毀掉它。

Yarden Laifenfeld: 但這不是普通的打印機。必須是正確的那臺打印機。

Kris Brandow: 而且你還必須以正確的方式砸它,因爲你得確保所有的芯片、內存,甚至打印鼓---如果是那種打印機---都要徹底毀掉。

Mat Ryer: 像《終結者》一樣。

Kris Brandow: 對,事情很多。

Mat Ryer: 把它扔進熔岩裏。這是一個方法。把它放進熔岩裏,然後慢慢融化。這個方法我從《終結者》系列裏學到的。

Kris Brandow: 工業碎紙機。

David Chase: 而且這是一臺打印機。它們從來沒對我們好過。

Mat Ryer: 是啊,它們確實是我們不得不面對的最糟糕的東西之一,不是嗎?雖然它們現在已經比以前好多了…… 但仍然是世界上最糟糕的東西,對吧?

Kris Brandow: 原始的物聯網設備,也是我們發明的最糟糕的設備之一。

Mat Ryer: 對,我討厭打印機。我不敢太大聲說,怕我的打印機聽到,下次就出問題了……

David Chase: 是的。

Mat Ryer: 以前經常遇到 “你無法打印,因爲打印機不在線” 的情況。然後你去按個按鈕讓它“上線”,然後它又說“哦,稍等一下……” 這功能到底是幹嘛的?爲什麼會有這種功能?爲什麼會有離線的選項?真的搞不懂,打印機……

Kris Brandow: 說到棧和堆,Yarden,你之前提到有時候我們想盡量把東西放在棧上,有時候則放在堆上…… 你有什麼建議嗎?比如什麼時候該控制這些,什麼時候該關注這些呢?

Yarden Laifenfeld: 你幾乎總是應該把東西放在棧上,這意味着---我自己的經驗是,我在開始學習 Go 之前寫了很長時間的 C。在 C 語言中,一個非常重要的做法是傳遞指針。這樣做的原因是不需要複製不必要的大結構,把它們從一個地方傳遞到另一個地方。我想這是一種過去的做法,那時候硬盤容量很小,我們想節省內存---但現在這不再是個大問題了;不過我就是這樣被教的,所以我把這種做法帶到了 Go 裏。但其實這是錯的,因爲任何時候你可以複製一個結構體,也就是直接傳遞它而不是傳遞它的指針,你都應該這麼做。因爲這樣一來,它可以被清理掉,而且你不需要給垃圾回收器額外的工作,去跟蹤指針的引用,判斷它指向哪裏,是否還有其他指針。

所以不要爲了節省內存而使用指針。這是我學到的一項重要內容。但你應該在需要共享某個東西的引用時使用指針。如果你遵循這個原則,你可能會獲得一些性能提升,特別是如果你之前一直沒有這麼做的話。

Mat Ryer: 這確實有點反直覺。我見過一些剛開始學習 Go 的人,他們一學會指針,就想通過指針傳遞所有東西,因爲他們覺得這樣可以避免複製。甚至沒有 C 語言經驗的人也會本能地認爲:“我只需要傳遞指針就好了,這樣更簡單。” 所以這確實很有趣。

David Chase: 抱歉,我剛剛在想這和調用約定(calling convention)有什麼關係。

Mat Ryer: 什麼是調用約定?

David Chase: 我們在使用寄存器(registers)的地方,我們是願意使用大量寄存器的。

Mat Ryer: 什麼是寄存器,David?

David Chase: 什麼是寄存器…… 抽象地說,在現代處理器開始做各種瘋狂的事情之前,你有一小部分暫存存儲區,它們是固定大小的,並且有固定的名字。0、1、2、3,一直到有時是 31,有時更多。你做的任何事情,最終都要將數據從內存中移入這些寄存器,然後在寄存器上進行操作,最後再存儲回內存。抱歉,我不太確定現在它們是如何實現的……

Mat Ryer: 現在一切都被高度抽象化了。

David Chase: 是的,非常抽象。現在有了推測執行(speculative execution)和超線程(hyper-threading),寄存器的名字和實際寄存器之間有了一層間接層。儘管如此,與一臺機器可能擁有的幾千兆字節內存相比,寄存器的數量仍然非常少。現在,寄存器的數量從 31 或 64 可能增加到幾百個,但仍然是一個固定的小數量。寄存器有固定的名字,而機器指令的大小也是固定的,有特殊的字段可以編碼這些小數字,代表寄存器的名字。這就是寄存器的定義。我希望這能解釋清楚……

你剛纔提到指針或不使用指針的問題。Go 語言有一個機制叫做逃逸分析(escape analysis),Java 也有一點,而其他編程語言則較少使用這個機制。有時候你確實需要創建一個指向某個東西的指針,可能是因爲你要調用一個別人寫的函數,而這個函數恰好需要一個指針。或者你希望共享某個對象並對其進行修改,那麼你就需要傳遞一個指針而不是整個結構體。Go 語言有一個特性,它的包導入沒有循環依賴。這意味着你可以先編譯運行時包,並且確定它不會依賴於其他任何東西。然後你可以逐層編譯依賴於運行時包的其他包……

所以對於每個函數來說,你可以判斷它是否保存了傳遞給它的指針。如果函數沒有將指針存儲到堆中或其他地方,那麼這個指針就不會逃逸,你可以把它指向的東西留在棧上。這就是所謂的逃逸分析。對於每個函數和方法,Go 會生成逃逸分析的總結,幫助你知道某個指針是否逃逸到堆中,或者是否泄漏給了其他線程。這是一種讓更多東西留在棧上的方法,而不是直接放到堆上。

Mat Ryer: 這種分析是在編譯時發生的嗎?還是在運行時?

David Chase: 目前這是在編譯時發生的。我們一直在討論如何做得更多、更好…… 我們內部有一些競爭的提案,大家來回討論,看看哪種方法能帶來最大的改進。這些改進是否會帶來運行時開銷?實現的風險有多大?是否會引發一些奇怪的 bug?有一種想法是,函數返回時它分配的內存不能放在它的棧上,因爲當它返回時,棧就消失了,所以它必須放在堆上。問題在於,是否可以改變調用約定,讓返回指針的函數使用某塊特定的內存區域,因爲調用者可能知道結果的生命週期,並且在使用完之後就不再需要它了。是否有辦法將內存傳遞給調用者?

在 Java 中,有些實現通過硬件來實現這個功能,例如 Azul 公司開發的硬件,它會嘗試將內存分配在棧上,但如果發現這是個壞主意,硬件會進行快速處理,並記錄下來,提醒下次不要在棧上分配這個對象。而因爲這是硬件實現,開銷非常小,不像軟件實現那樣慢。那麼問題就在於,在軟件中我們能走多遠?這是否值得?

Go 的垃圾回收器相比其他語言的垃圾回收器,比如 Java 的垃圾回收器,分配速度可能慢一些,回收垃圾的速度也慢一些。但它的優勢是不會移動內存,能夠處理內部指針,且擁有極小的全局暫停時間。所以這裏有一些有趣的權衡。雖然使用指針的開銷不大,但通過避免堆分配,你可能在某些情況下仍然可以獲得性能收益。

Kris Brandow: 是的,我在編寫代碼時確實做過一些優化,確保我寫的東西不會逃逸到堆中。我會有意識地寫代碼,確保逃逸分析能判斷出這些內容會留在棧上。

我記得我們之前在工具相關的那期節目裏提到過:就像代碼覆蓋率一樣,我希望有一種分析工具可以在編輯器中把變量標記成不同的顏色,比如綠色或紅色,告訴我哪些變量會逃逸到堆中,哪些會留在棧上。

如果有這樣一種工具,可以更直觀地顯示編譯器或者分析工具的判斷,我覺得會非常有用。因爲目前我們缺乏這樣的工具,很多時候只能依靠直覺,然後事後分析代碼,看看它們到底是在堆上還是棧上。所以如果能有一種更直觀的方式展示這些信息,我覺得會非常有幫助。

David Chase: 這裏有兩個答案,至少有兩個答案。我們曾經和一些對性能非常關注的 Go 用戶進行過討論,也和一些 IDE 開發者進行過交流。編譯器有一個標誌,可以輸出一些信息,雖然對人來說有些麻煩,但它是爲自動化工作流設計的。你可以使用這個標誌,-json=0,目錄名,它會爲所有編譯的包生成 JSON 文件,記錄編譯器的所有失敗信息,比如 “對不起,無法去抖動檢查。” 或者 “對不起,不得不進行堆分配。” 這些信息都是以 LSP 格式(VS Code 使用的格式)輸出的。雖然它有點複雜,但你可以把這些信息導入 IDE 中。不過問題是,編譯器經常出錯,它會不停地記錄這些失敗信息。但大多數時候這些錯誤並不重要,因爲絕大部分時間你只是在一些關鍵地方遇到瓶頸。

所以你需要結合性能分析工具,專注於那些真正有問題的地方,而不是所有地方都看。否則你會覺得 “編譯器太糟糕了,看看它爲我做了多少壞事。” 我們正在處理這個問題,PGO(性能指導優化)功能即將到來…… 但目前還只是實驗性功能,未來會有更多更新。(譯者注: 1.18 已經有 PGO 功能)

Mat Ryer: 你提到這不是爲人類設計的,因爲信息太過繁雜……

David Chase: 是的,我覺得確實不是爲人類設計的,信息太多。我認爲需要一個過濾步驟,比如查看性能分析報告,專注於某個函數,看看它的壞消息是什麼。

Mat Ryer: 那你會加個 CAPTCHA 驗證嗎?讓人們點擊 “我是機器人”?這是一個方法。

David Chase: 不,我們沒有這樣做。

Kris Brandow: 我覺得這確實是個好主意。就像我之前提到的那樣,如果這種工具能集成到 Gopls 或其他語言服務器中,那就太好了。這樣我們就可以在需要優化的地方使用它,像運行覆蓋率測試一樣。我覺得這確實是個好方法,但你也不希望人們在某個只調用了一次的函數上過於糾結,試圖讓它避免堆分配。就像反射一樣,有些人會擔心在啓動時使用反射會帶來性能問題,但其實只要它只在啓動時調用一次,完全沒問題。

Mat Ryer: 這確實提出了一個很好的觀點---我們經常提到 “先測量,再優化”。但這也引發了一個很好的問題,既然 Go 團隊一直在幕後忙着進行優化和改進,那麼是否有可能我們優化了代碼,結果新版本的 Go 出來了,導致我們的優化變得多餘,或者甚至使我們的代碼性能下降?我們是否應該不斷重新測量和評估?

David Chase: 我不會說永遠不可能……

Mat Ryer: 你剛纔說了兩次 “永遠不可能”,David……

David Chase: 是的,我說了。我會說,永遠不要說超過兩次……

Mat Ryer: 已經四次了,繼續吧。

David Chase: 好的……

Mat Ryer: 不好意思,我只是故意打斷你。

David Chase: 對,我認爲我們一個目標就是讓你們過去的一些辛苦工作變得不再必要。我一直在想,“我們能不能重新排列字段,來讓結構體更緊湊,而不用再告訴大家應該自己去手動排序字段呢?” 這樣我們就可以讓優化指南更簡短一些。然後你們之前辛苦調整字段順序的工作---抱歉,那可能是浪費時間了。

Yarden Laifenfeld: 如果有人真的在意字段的順序的話……

Mat Ryer: 二進制編碼時順序確實很重要……

Yarden Laifenfeld: 是的,類似這樣的情況。

David Chase: 順便說一句,這不會很快實現,但只是一個想法。如果我們做到了,那就可以讓優化步驟少一步。不過通常情況下,我們不會這麼做。人們不喜歡他們的代碼變慢。你可能還記得 Spectre 和 Meltdown 漏洞 [12] 剛出來的時候…… 有時候安全補丁會讓你的代碼變慢。

有一次---我記不清在哪裏看到的---有關於 Java 字符串的一個可怕問題,涉及到兩種不同的編碼方式。你可以傳遞它們到某個地方,可能會發生競爭狀態。基本上,它驗證數據,然後使用數據。但因爲它是先驗證再使用,所以可能有線程會競爭並在此期間破壞數據,導致數據不再有效。

Mat Ryer: 哦,如果你在這兩個操作之間插手的話。

David Chase: 對,對,對。所以你可能會遇到非常奇怪的行爲。我相信他們會修復這個問題,而修復的方法是必須添加一次拷貝操作。你會先複製它,然後驗證,接着使用一個別人無法修改的版本,這樣就增加了一些開銷。Spectre 和 Meltdown 也是類似的情況,你覺得你的處理器很快很酷?結果我們要讓它變慢一點,而且你還得生成不同的代碼。如果這是你的問題,那就只能接受它變慢了。

當你第一次聽說 Spectre 和 Meltdown,還有 Rowhammer[13],你會覺得 “你做了什麼?天吶!” 因爲你希望硬件能正常運作,而不是給你帶來這些麻煩。

Kris Brandow: 是啊。我想回到我們之前討論的結構體字段重新排列問題,有時候按順序排列字段讓它們更緊湊,確實不錯,可以節省空間。但由於緩存的工作機制,有時候你反而希望它們不要緊挨在一起,你會想 “不要,我希望這些東西在不同的緩存行上。” 如果它們在同一緩存行上,我的性能就會大大下降。所以你可以看到,自動爲用戶解決這個問題會有多麼複雜。

David Chase: 但這不是 Go 101 的內容,我也不認爲我們會自動解決這個問題。

Kris Brandow: 是的。

David Chase: 我是說,這也不是你會教給初學 Go 程序員的東西。

Kris Brandow: 對,我只是說如果你重新排列結構體的字段,可能會把某些東西放在不該挨在一起的緩存行上,導致性能問題。

David Chase: 你還得有一種方式來規定與其他編程語言或操作系統的交互。事情必須按某種方式排列,如果不是,那就太糟了。

Kris Brandow: 對。

Mat Ryer: 說到這兒,我們到了該分享不受歡迎觀點的環節了。

Mat Ryer: 好了,Yarden,你有什麼不受歡迎的觀點嗎?

Yarden Laifenfeld: 有的。我認爲這個觀點真的很不受歡迎。我的觀點是,Go 不應該再增加任何大功能。

Mat Ryer: 哦,爲什麼呢?

Yarden Laifenfeld: 我喜歡簡單。我喜歡簡潔。你之前問我最喜歡的編程語言是什麼,我說了 C 和 Go。我認爲原因是,當我看 Go 代碼或 C 代碼時,我知道它們在做什麼。而如果我看 Java 代碼,如果有人使用了不同的編碼規範,或者使用了我不熟悉的新功能,那麼這段代碼對我來說可能幾乎是不可讀的。而我覺得 Go 在保持簡單這一點上做得非常好。我喜歡它真的強制你遵循某種結構,而不像 Python,那幾乎讓你可以隨意編寫代碼…… 甚至 Go 的靜態檢查工具也會告訴你 “這不是 Go 中命名變量的正確方式。” 我非常喜歡這一點。我覺得這真的有助於快速入門,幫助閱讀他人的代碼,也幫助寫出好的代碼…… 因爲你只需要學習基礎知識,然後在此基礎上構建,而不是不斷學習新的東西,這樣你會比不斷學習新東西更快地成爲一個更好的開發者。

所以總的來說,如果我們增加任何新的大功能,我們就會偏離這個理念。而且我認爲大多數大功能都會違背這個理念,不會給語言用戶帶來太多價值。當然,我不是 Go 團隊的一員,我也沒有數據支持我的觀點。

Mat Ryer: 是啊。

Yarden Laifenfeld: 這不是基於統計數據的。

Mat Ryer: 你這是在讓 David 失業啊。但讓我問你這個問題…… 那泛型呢?你對它有什麼看法?

Yarden Laifenfeld: 我想你應該能猜到我對泛型的看法了,Mat……[笑聲] 我真的很喜歡只有一種方式可以做事。我知道…… 嗯,我知道我們已經作爲開發者成長了,我希望我們能回到過去…… 開玩笑的。但我確實喜歡我們在進步,喜歡事情變得更抽象化,但我也喜歡 Go 讓你始終保持接近事物本質。你確實需要知道指針是什麼…… 如果你聽了這個播客,你就知道棧是什麼…… 這些重要的東西。所以---天啊,我完全忘記我剛纔說到哪兒了。

Mat Ryer: 沒關係。那麼,David,你現在有沒有考慮增加一些大功能呢?你怎麼看這個觀點?

David Chase: 我對泛型的接受度很感興趣…… 我們在討論擴展迭代器,使它們更通用。而且有人在討論協程,一旦協程不是---協程不會是 Goroutine,因爲用 Goroutine 實現協程速度不夠快。

Kris Brandow: 我確實覺得協程是我想要的一個功能。

Yarden Laifenfeld: 問題是,這些都是我想要的功能。比如,我以前是非常想要泛型的。在泛型沒有出現之前,我一直在想 “天啊,我需要泛型。” 但由於我們需要支持非常古老的版本,我還沒有機會用它們…… 我心情矛盾,但這仍然是我的不受歡迎的觀點。我堅持這個想法。

Mat Ryer: 很好。David?

David Chase: 我有好幾個不受歡迎的觀點。我不認爲它們比 “不要增加新功能” 更好,但……

Mat Ryer: 這已經很不受歡迎了。

Yarden Laifenfeld: 不,不是不要增加新功能。順便說一句,我認爲標準庫可以增加一些新功能……

David Chase: 啊……[笑]

Yarden Laifenfeld: 我覺得增加一些像 “max” 這樣的功能不算壞。

Mat Ryer: Yarden,這不是對 David 的績效評估啊。[笑]

Yarden Laifenfeld: 不,不好意思…… 我只是想說,我感到非常抱歉。我真的非常抱歉。請繼續開發 Go。你們做得很好。

David Chase: 我不確定…… 我的其中一個不受歡迎的觀點可能更多是 Go 團隊內部不受歡迎的,挺技術性的。多年前,我與 Fortran 有很大關聯。我的導師被稱爲 “Fortran 博士”。

Mat Ryer: 聽起來像是個反派。

David Chase: 我曾在 John Backus[14] 先生手下實習。 (譯者注: 約翰 · 巴科斯, FORTRAN 開發小組組長, 提出了巴科斯範式, 1977 年圖靈獎得主)

Mat Ryer: 哇哦。

David Chase: 是啊,挺酷的。

Mat Ryer: 這確實很酷。

David Chase: 所以我對 Fortran 有一種特殊的情感。讓 Fortran 快的關鍵在於一個非常小的東西,而大多數程序中這通常是正確的。那就是,當你傳遞一對切片給一個函數時,Fortran 規定你不能假設它們重疊。如果它們重疊,那就不是 Fortran 了。而這是一個有趣的規則,無法通過語法檢查,但如果你的代碼傳遞重疊的內存給函數,而它能檢測到,那麼它就不符合 Fortran 規範。因此,這是處理所有 bug 報告的一個便利方式。如果你的代碼能檢測到這種情況,它會說:“這沒問題,但這不是 Fortran 的問題,這是其他語言的問題,自己想辦法吧。”

但這樣做的好處是,它可以讓你毫無顧忌地進行向量化(Vectorization)、並行化、以及重新排序等各種優化。這幾乎是 Fortran 快速的關鍵之一。因此,我有時候會想---有很多關於不同語言之間調用和數據轉換的煩惱,這從來都不有趣,總是令人煩惱。

所以,如果你說 “我們可以讓 Go 代碼變快”,如果你做了這個---這需要大量的編譯器工作,並且對像我這樣的人來說意味着更多的工作機會,這很好…… 但你可以通過修改參數規則來讓代碼更快。爲了讓這從上個世紀或上個千年走出來,我會說:“機器學習!哇哦!” [笑] 所以這就是我的不受歡迎的觀點,可能在 Go 團隊內部更不受歡迎。

Mat Ryer: 他們大概能明白你的意思。[笑]

David Chase: 是的。我擔心這就是問題所在。另一個問題是持有不太受歡迎的觀點,而人們並不完全理解。

Mat Ryer: 某種程度上,這也挺聰明的。David,你還寫下了另一個觀點,我挺喜歡的,就是 Go 語言需要更大的整數類型。int128int256int512... David,你打算用這些巨大的整數幹嘛?

David Chase: 現在的處理器都有這些瘋狂的額外指令,它們需要巨大的輸入和操作數。而在 C 語言中,這些被編碼得很糟糕---出於某種原因,他們爲 C 語言的整數類型選擇了愚蠢的名字。當他們需要使用這些大整數類型時,又不得不選擇更愚蠢的名字。你用它們的時候,還得包含一個特殊的頭文件,然後污染你的代碼,充斥着這些糟糕的名字。而 Go 語言可以將這些指令作爲內置函數,並且 Go 可以爲這些輸入類型起一個完全合理的名字:int128int256int512。我認爲這很不錯。我們可以實現這一點。我們已經在 32 位機器上處理 64 位整數了,這在編譯器中並不難。因爲有人問我們:“我們真的想用這些內置函數,我們想這麼做”,於是我們不斷討論最佳方式,但我們沒有合適的類型,所以我們只能用結構體做這種很 hack 的事情,但那個結構體是特殊的... 不如直接說:int128,搞定。

Kris Brandow: 這讓我想起了---我記得是羅布 · 派克(Rob Pike)提過一個提案,想把intuint改爲任意精度整數…… 我當時想:“我喜歡這個。我想在我的語言中直接使用任意精度。” 但…

David Chase: 這可行。我覺得有趣的是,是否可以研究一種默認行爲,或者讓你可以選擇是否編譯代碼,使得如果溢出---一個反提案是,**如果有符號整數溢出,程序直接崩潰_;這是個小小的安全問題,但對 Go 來說可能沒那麼嚴重_。確實有一些利用整數溢出的漏洞,但這些漏洞還利用了 “哈哈!那些人沒有檢查他們的數組邊界。他們以爲檢查了數組邊界,但我們讓整數溢出了。我們可以玩了。” 而 Go 會直接說:“不,我們檢查了你的數組邊界,走開吧。”**

Kris Brandow: 對。

David Chase: 這可能不必要,但這是一個反提案。我認爲這些大整數可能不會放在棧上,嗯,也許有時候會放在棧上,但它們必須有一個選項讓它們不放在棧上。

Kris Brandow: 說得對,是的。

Mat Ryer: 好的,那我們就以這個話題爲結尾吧---謝謝你,David,把我們拉回到棧這個話題,並且爲今天的討論畫上了一個完美的句號…… 非常感謝你。Yarden,非常感謝你爲社區做的所有工作。以色列的 GopherCon,如果有人在那片區域,想要去見見 Go 語言的開發者們,那就趕緊加入吧…… 還有 “Women Who Go Israel” 組織……Go 語言社區的美妙之處就在這裏,像你這樣的人投入了大量的工作。如果你不親自參與,你可能根本不知道這需要多少工作。我只是在旁邊看了一眼就知道人們付出了多少努力,所以真的非常感謝你。

David Chase---顯然,你在 Go 團隊上的所有工作…… 我們還能說什麼呢?非常感謝你做的這一切。Kris,我沒什麼好感謝你的,除了你能出現並做你自己之外。還有謝謝我們的聽衆們,感謝你們的收聽…… 因爲說實話,如果沒有你們,這一切真的都沒有意義。我是 Mat Ryer,我也要感謝我自己…… 沒人會這麼做,那我自己說吧。非常感謝大家,我們下次在 Go Time 再見

參考資料

[1]

#288 A deep dive into Go's stack: https://changelog.com/gotime/288

[2]

Mat Ryer: https://www.linkedin.com/in/matryer

[3]

Kris Brandow: https://github.com/skriptble

[4]

Yarden Laifenfeld: https://github.com/yardenlaif

[5]

Rookout: https://www.rookout.com/

[6]

Dynatrace: https://www.dynatrace.com/

[7]

GopherCon Israel: https://www.gophercon.org.il/

[8]

David Chase: https://github.com/dr2chase

[9]

George Carlin: https://www.imdb.com/name/nm0137506/

[10]

《Changelog & Friends》: https://changelog.com/friends

[11]

Oxide Computer: https://oxide.computer/

[12]

Spectre 和 Meltdown 漏洞: https://www.cloudflare.com/zh-cn/learning/security/threats/meltdown-spectre/

[13]

Rowhammer: https://www.cnblogs.com/zhanggaoxing/p/18099550

[14]

John Backus: https://www.ibm.com/history/john-backus

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/LIQms9-XoEEmgRxppEHvVw