『每周譯Go』Go sync map 的內(nèi)部實現(xiàn)
目錄
引言 a. 簡單介紹并發(fā)性及其在此上下文中的應(yīng)用 sync.RWMutex 和 map 一起使用的問題 介紹 sync.Map a. 在哪些場景使用 sync.Map? sync.Map 實現(xiàn)細節(jié) a. 加載、存儲和刪除如何在高層次上工作?b. 存儲被刪除和只是 nil 之間的區(qū)別 c. Store(key, value interface) d. Load(key interface{}) (value interface{}, ok bool) e. LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) f. LoadAndDelete(key interface{}) (value interface{}, loaded bool) g. Delete()(value interface{}, ok bool) h. Range(f func(key, value interface{}) bool)_ sync.Map 和 RWMutex 保護的 map 之間的性能對比??焖賰?yōu)化指南 極致的優(yōu)化是什么樣的?
引言
這篇文章簡單介紹了如何使用 sync.Map,同時解釋 sync.Map 的工作原理。
在應(yīng)用代碼中,大多數(shù)操作都是依賴 hash map。因此,如果 hash map 的讀寫很慢,它們最終會成為性能的主要瓶頸。在引入 sync.Map 之前,標準庫只需要依賴于不是線程安全的內(nèi)置 map。當多個 goroutines 調(diào)用它時,必須依賴 sync.RWMutex 來同步訪問。但是這么做,并不能很好地利用多核 CPU 的性能,所以在 go1.9 引入 sync.Map 來解決多核 CPU 操作 map 的性能問題。當程序運行在 64 核的機器上,sync.RWMutex 的性能下降會比較明顯。
使用sync.RWMutex,sync.RWMutex中readerCount的值隨著每次調(diào)用讀鎖而增加,這會導(dǎo)致緩存的競爭。最終導(dǎo)致處理器其他內(nèi)核的性能下降。這是因為每增加一個核心,更新每個核心緩存的開銷就會增加。sync.Map 文檔
snyc.Map 類型針對兩個常見案例進行優(yōu)化
(1) 當給定鍵的 entry 只寫入一次但讀取多次時,就像只會增長的緩存中一樣
(2) 當多個 goroutine 讀取、寫入和重寫 keys 。在這兩種情況下,與使用單獨的 Mutex 或 RWMutex 配對的 map 相比,使用 sync.Map 可以顯著減少鎖爭用。
帶有讀寫鎖的 map 在第一個 case 中,在頻繁地調(diào)用atomic.AddInt32(&rw.readerCount, 1) and atomic.AddInt32(&rw.readerWait, -1) 下性能顯著下降。寫入的開銷無法避免,但我們必須期望讀取速度非??欤貏e是它作為一個緩存,應(yīng)該有助于在快速管道中處理數(shù)據(jù)。
簡單介紹并發(fā)性及其在此上下文中的應(yīng)用
現(xiàn)代應(yīng)用程序必須在短時間內(nèi)處理大量歷史數(shù)據(jù)。為此,他們重度依賴高速的多核處理器。
這些系統(tǒng)處理的實時輸入通常具有不可預(yù)測的到達時間。這正是操作系統(tǒng)級線程和 goroutines 真正閃耀的地方。這些依賴鎖和信號量的組合來讓它們在等待輸入時 "休眠"。這會釋放虛擬線程使用的 CPU 資源,直到它收到喚醒它的中斷。等待中斷的線程或 goroutine 類似于等待在輸入到達時調(diào)用的回調(diào)——中斷信號在解鎖它正在等待的互斥鎖時(或當信號量變?yōu)榱銜r)產(chǎn)生。
在理想情況下,我們希望應(yīng)用程序處理數(shù)據(jù)的能力隨著計算機核心數(shù)增加而增強。即使程序一開始的設(shè)計是多個 goroutines 處理多個任務(wù),也并不一定能夠很好地擴展算力。
某些代碼塊可能需要鎖或 atomic 指令進行同步,將這部分 (譯者注:臨界區(qū)) 變成強制同步執(zhí)行的代碼塊。比如每個 goroutine 都可以訪問的中央緩存。這些會導(dǎo)致鎖爭用,從而阻止應(yīng)用程序通過增加內(nèi)核來提高性能。甚至更糟的是,這些可能會導(dǎo)致性能下降。atomic 指令也會帶來開銷,但它比鎖引起的開銷小得多。
硬件級原子指令對于內(nèi)存的訪問并不總是保證讀取最新值。原因是,每個處理器內(nèi)核都維護一個可能失效的本地緩存。為了避免這個問題,原子寫操作通常跟在指令之后強制每個緩存更新。最重要的是,為了提升(在硬件和軟件級別)的性能,它還必須防止 內(nèi)存重新排序(就是我們常說的指令重排,cpu 的一種優(yōu)化手段)。
map 結(jié)構(gòu)被廣泛使用,以至于幾乎每個應(yīng)用程序都在其代碼中依賴它。并且要在并發(fā)應(yīng)用程序中使用它,讀取和寫入它必須與 Go 中的 _sync.RWMutex_ 同步。這樣做會導(dǎo)致對 atomic.AddInt32(...)的過度使用,從而導(dǎo)致頻繁的緩存爭用,強制刷新緩存和內(nèi)存(指令)排序。這會降低性能(譯者注: 這里說的緩存是 CPU 中的緩存)。
sync.Map使用 atomic 指令和鎖的組合,但確保讀取操作的路徑盡可能短,大多數(shù)情況下,每次調(diào)用 Load(...) 只需一個原子加載操作案件。atomic 存儲指令通常是強制更新(每個內(nèi)核的)緩存的指令,而 atomic 加載可能只需要強制執(zhí)行內(nèi)存排序并確保其原子性。只有 atomic.AddInt32(...) 最糟糕,因為它與對同一變量的其他原子更新的爭用將導(dǎo)致它忙于等待,直到依賴于比較和交換(譯者注:這里便是CAS指令,在匯編中一個指令完成兩個操作)指令的更新成功。
sync.RWMutex 和 map 一起使用的問題
使用 sync.RWMutex 來同步訪問 map 的一個例子: https://github.com/golang/go/blob/912f0750472dd4f674b69ca1616bfaf377af1805/src/sync/map_reference_test.go#L25
為方便起見,將上面的代碼鏡像到這里:
源代碼 1
// Taken from here: https://github.com/golang/go/blob/912f0750472dd4f674b69ca1616bfaf377af1805/src/sync/map_reference_test.go#L25
// RWMutexMap is an implementation of mapInterface using a sync.RWMutex.
type RWMutexMap struct {
mu sync.RWMutex
dirty map[interface{}]interface{}
}
func (m *RWMutexMap) Load(key interface{}) (value interface{}, ok bool) {
m.mu.RLock()
value, ok = m.dirty[key]
m.mu.RUnlock()
return
}
func (m *RWMutexMap) Store(key, value interface{}) {
m.mu.Lock()
if m.dirty == nil {
m.dirty = make(map[interface{}]interface{})
}
m.dirty[key] = value
m.mu.Unlock()
}
func (m *RWMutexMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
m.mu.Lock()
actual, loaded = m.dirty[key]
if !loaded {
actual = value
if m.dirty == nil {
m.dirty = make(map[interface{}]interface{})
}
m.dirty[key] = value
}
m.mu.Unlock()
return actual, loaded
}
func (m *RWMutexMap) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
m.mu.Lock()
value, loaded = m.dirty[key]
if !loaded {
m.mu.Unlock()
return nil, false
}
delete(m.dirty, key)
m.mu.Unlock()
return value, loaded
}
func (m *RWMutexMap) Delete(key interface{}) {
m.mu.Lock()
delete(m.dirty, key)
m.mu.Unlock()
}
func (m *RWMutexMap) Range(f func(key, value interface{}) (shouldContinue bool)) {
m.mu.RLock()
keys := make([]interface{}, 0, len(m.dirty))
for k := range m.dirty {
keys = append(keys, k)
}
m.mu.RUnlock()
for _, k := range keys {
v, ok := m.Load(k)
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
當性能很重要時,我們可以通過減少并行獲取相同鎖的頻率(從而減少鎖爭用的頻率)或完全用 atomic 指令(如 atomic-load、atomic-store 和 atomic-compare)代替鎖來解決這個問題和交換。atomic 操作也不是靈丹妙藥,因為依賴于 atomic 比較和交換的狀態(tài)更新在無限循環(huán)中運行,直到更新成功。當存在爭用時,更新通常不會發(fā)生,因此當有大量并發(fā)更新時,導(dǎo)致它們忙等待。
大多數(shù)應(yīng)用程序通常使用兩者的組合。甚至有些應(yīng)用程序也嘗試選擇更快的替代方案,例如減少對循環(huán)中旋轉(zhuǎn)的 atomic 比較和交換指令的調(diào)用次數(shù)。
[sync.RWMutex](https://github.com/golang/go/blob/912f0750472dd4f674b69ca1616bfaf377af1805/src/sync/rwmutex.go#L28) 使用信號量的組合以及兩個附加變量 readerCount和 readerWait 來記錄正在讀取和等待讀取的數(shù)量。
要理解在多核環(huán)境中我們需要使用 sync.Map,而不是使用由 sync.RWMutex 保護的內(nèi)置 map,那么,我們必須深入研究how [sync.RWMutex](https://sreramk.medium.com/go-sync-rwmutex-internals-and-usage-explained-9eb15865bba) 的內(nèi)部工作。
sync.Map 有下面這些一看就懂的方法(取自 這里)
源代碼 2
//Delete deletes the value for a key.
func (m *Map) Delete(key interface{})
//Load returns the value stored in the map for a key, or nil if no
//value is present. The ok result indicates whether value was found
//in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool)
//LoadAndDelete deletes the value for a key, returning the previous
//value if any. The loaded result reports whether the key was
//present.
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)
//LoadOrStore returns the existing value for the key if present.
//Otherwise, it stores and returns the given value. The loaded
//result is true if the value was loaded, false if stored.
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
//Range calls f sequentially for each key and value present in the
//map. If f returns false, range stops the iteration.
//Range does not necessarily correspond to any consistent snapshot
//of the Map's contents: no key will be visited more than once, but
//if the value for any key is stored or deleted concurrently, Range
//may reflect any mapping for that key from any point during the
//Range call.
//Range may be O(N) with the number of elements in the map even if f
//returns false after a constant number of calls.
func (m *Map) Range(f func(key, value interface{}) bool)
//Store sets the value for a key.
func (m *Map) Store(key, value interface{})
在繼續(xù)嘗試了解 sync.Map 的原理之前,還要了解 為什么我們絕對需要這些方法 [LoadAndDelete (...)](https://sreramk.medium.com/go-why-does-sync-map-97342f12b3fa)和 [LoadOrStore(...)](https://sreramk.medium.com/go-why-does-sync-map-97342f12b3fa)。
sync.Map總是維護兩個 map 對象,一個用于讀取,一個用于寫入。讀映射是部分不可變的,而寫映射是完全可變的。
在任何時候,可讀 map 都不會更新,而可寫的 map 是可以更新,表示存儲的準確狀態(tài)(即使它沒有設(shè)置為 nil)。每當從只讀 map 未命中鍵時,它就會從帶有 sync.Mutex 鎖的寫入映射中讀取。
刪除一個鍵的時候,鍵可以只存在于可寫 map 中,也可以存在于可寫 map 和可讀 map 中。如果它僅存在于可寫 map 中,則使用內(nèi)置的 delete 操作將其完全刪除。但是如果它同時存在于可寫 map 和可讀 map 中,則它只設(shè)置為nil(注意,將字段設(shè)置為 nil 的更新操作同時反映在讀 map 和寫 map 中,因為它們指向同一個 entry 對象;稍后會詳細介紹)。
每次訪問由 sync.Mutex 保護可寫的 map,也有原子指令的開銷。因此,這些開銷略大于由 sync.RWMutex 保護的內(nèi)置 map。
因此,此實現(xiàn)的目標之一應(yīng)該是減少讀取未命中的頻率。這是通過將可寫 map 提升為下一個可讀的 map 來完成的(同時將指向可寫 map 的指針設(shè)置為 nil),當讀取未命中數(shù)大于可寫 map 的長度時(此長度使用 len )。然后丟棄過時的可讀 map。
在第一次嘗試向可寫的 map 添加鍵時,通過將新可讀 map 的內(nèi)容復(fù)制到可寫的 map 來創(chuàng)建新 map 對象。數(shù)據(jù)搬移結(jié)束之后,可讀的 map 將存儲最準確的狀態(tài)。但是被搬移之后就變成了 "不可變",不會再有新的 key 被添加進去。因此,向 sync.Map 添加額外的鍵會使可讀的 map 失效。
更新有兩種不同的方式。如果添加的鍵(使用sync.Map 的Store(...) 或LoadOrStore(...) 操作)已經(jīng)存在于可讀的 map 中,則可讀的 map 中與鍵關(guān)聯(lián)的值是以原子方式更新(注意:以這種方式對 value 字段所做的更改也將立即反映在可寫映射上。本文稍后將解釋這是如何做到的)。如果鍵只存在于可寫的 map 中,或者鍵根本不存在,則更新僅在可寫區(qū)域中進行,并需要使用 “sync.Mutex”鎖。
sync.Map 的使用場景
sync.Map 最初是為了減少 Go 的標準庫包所產(chǎn)生的開銷,這些包一直使用由 sync.RWMutex 保護的 map。所以 Go 的作者們發(fā)現(xiàn)像 sync.Map 這樣的存儲不會增加太多的內(nèi)存開銷(另一種內(nèi)存密集型解決方案是為每個使用的 goroutine 提供一個單獨的映射副本,但讓它們同步更新)而還提高程序在多核環(huán)境中的性能。因此,sync.Map 的初衷是為了解決標準包中的問題,但還是公開了,希望大家覺得有用。
但是,文檔并沒有真正詳細說明 sync.Map 究竟在哪里最有用。通過基準測試 和 [sync.Map](https://medium.com/@deckarep/the-new-kid-in-town-gos-sync-map-de24a6bf7c2c) 表明,僅當它在具有超過 4 個核的系統(tǒng)上運行時,使用 sync.Map 比使用由 sync.RWMutex 保護的 map 更加高效。sync.Map 最理想的用例是將其用作緩存,特別是頻繁訪問不相交的鍵;或從同一組鍵中廣泛讀取它。
新添加的鍵可能會留在可寫的 map 中,而最常訪問的鍵可能會留在可讀的 map 中。當添加、讀取和刪除一組有限的鍵時,假定每個操作都以相同的頻率發(fā)生時,sync.Map 將執(zhí)行最少的操作。當你不讓寫 map 中的鍵被提升時,就會發(fā)生這種情況經(jīng)常添加和刪除它們。在這種情況下,我們最好將 map 與 sync.RWMutex 或 sync.Mutex 一起使用(并發(fā)散列映射的確切選擇通常通過基準測試決定)。
每當一個鍵在 sync.Map 中被刪除,它只將其關(guān)聯(lián)的值字段標記為[nil](https://github.com/golang/go/blob/21a04e33353316635b5f3351e807916f3bb1e844/src/sync/map.go#L297),但直到第一次寫入后,key 才真正刪除 writable-map 作為可讀 map。這會導(dǎo)致內(nèi)存開銷。但是這種開銷只是暫時的,等到下一個提升周期,開銷就會下降。
sync.Map 實現(xiàn)細節(jié)
方便起見,這里給出 [sync.map](https://github.com/golang/go/blob/21a04e33353316635b5f3351e807916f3bb1e844/src/sync/map.go#L12) 的結(jié)構(gòu)。
源代碼 3
// Map is like a Go map[interface{}]interface{} but is safe for concurrent use
// by multiple goroutines without additional locking or coordination.
// Loads, stores, and deletes run in amortized constant time.
//
// The Map type is specialized. Most code should use a plain Go map instead,
// with separate locking or coordination, for better type safety and to make it
// easier to maintain other invariants along with the map content.
//
// The Map type is optimized for two common use cases: (1) when the entry for a given
// key is only ever written once but read many times, as in caches that only grow,
// or (2) when multiple goroutines read, write, and overwrite entries for disjoint
// sets of keys. In these two cases, use of a Map may significantly reduce lock
// contention compared to a Go map paired with a separate Mutex or RWMutex.
//
// The zero Map is empty and ready for use. A Map must not be copied after first use.
type Map struct {
mu Mutex
// read contains the portion of the map's contents that are safe for
// concurrent access (with or without mu held).
//
// The read field itself is always safe to load, but must only be stored with
// mu held.
//
// Entries stored in read may be updated concurrently without mu, but updating
// a previously-expunged entry requires that the entry be copied to the dirty
// map and unexpunged with mu held.
read atomic.Value // readOnly
// dirty contains the portion of the map's contents that require mu to be
// held. To ensure that the dirty map can be promoted to the read map quickly,
// it also includes all of the non-expunged entries in the read map.
//
// Expunged entries are not stored in the dirty map. An expunged entry in the
// clean map must be unexpunged and added to the dirty map before a new value
// can be stored to it.
//
// If the dirty map is nil, the next write to the map will initialize it by
// making a shallow copy of the clean map, omitting stale entries.
dirty map[interface{}]*entry
// misses counts the number of loads since the read map was last updated that
// needed to lock mu to determine whether the key was present.
//
// Once enough misses have occurred to cover the cost of copying the dirty
// map, the dirty map will be promoted to the read map (in the unamended
// state) and the next store to the map will make a new dirty copy.
misses int
}
正如我們所見,sync.Map 有一個 dirty map 存儲和一個用于存儲 “clean” 的 read map 的 atomic.Value 字段。所有對 dirty 的訪問總是由 mu 保護。在我們查看每個獨立的方法如何工作之前,我們必須對 sync.Map 的工作原理和設(shè)計思想有一個宏觀的了解。
The [鍵值對構(gòu)成的 entry ](https://github.com/golang/go/blob/c1cc9f9c3d5ed789a080ef9f8dd9c11eca7e2026/src/sync/map.go#L73)結(jié)構(gòu)對于sync.Map的功能至關(guān)重要
源代碼 4
type entry struct {
// p points to the interface{} value stored for the entry.
//
// If p == nil, the entry has been deleted, and either m.dirty == nil or
// m.dirty[key] is e.
//
// If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
// is missing from m.dirty.
//
// Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
// != nil, in m.dirty[key].
//
// An entry can be deleted by atomic replacement with nil: when m.dirty is
// next created, it will atomically replace nil with expunged and leave
// m.dirty[key] unset.
//
// An entry's associated value can be updated by atomic replacement, provided
// p != expunged. If p == expunged, an entry's associated value can be updated
// only after first setting m.dirty[key] = e so that lookups using the dirty
// map find the entry.
p unsafe.Pointer // *interface{}
}
entry 結(jié)構(gòu)相當于 “容器”或“盒子”,保存和鍵相關(guān)聯(lián)的值。不是直接用 m[key] = value 存儲值,而是將 value 封裝在一個方便的 entry 類型容器中。然后將 entry 的地址存儲到與鍵關(guān)聯(lián)的 map 對象中。
在任何的時間節(jié)點中,都假定 entry 滿足以下三個屬性之一:
它包含一個有效值:
p != expunged && p != nil。expunged(意味著永久刪除)被定義為var expunged = unsafe.Pointer(new(interface{})),這是一個只設(shè)置一次的任意全局值。它沒有任何價值。它只是為了將 “刪除” 值與任意entry字段中的所有其他nil和非nil值區(qū)分開來。它包含
nil。它包含
expunged(我們很快就會詳細介紹 nil 狀態(tài)與 expunged 狀態(tài)有何不同)。
將指針包裹在一個像 entry 這樣的結(jié)構(gòu)下是非常方便的。如果您正在更改 entry 的狀態(tài),您可以期望它在每個其他具有指向 entry 對象的指針的位置立即更新。
所以,如果您在可讀的 map 和可寫的 map 之間共享 entry 的地址,更新可讀 map 的 entry也將反映可寫的 map 上的更改以及反之亦然(請注意,在源 3 和可寫映射中)源 5 映射被定義為map[inerface{}]entry,其中鍵是一個interface{},并且該 entry 存儲為一個指針)。
源代碼 5:只讀結(jié)構(gòu)體
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
m map[interface{}]*entry
amended bool // true if the dirty map contains some key not in m.
}
sync.Map 結(jié)構(gòu)的 read 字段是一個 atomic.Value 字段,它保存對 readOnly(源代碼 5)的引用。此結(jié)構(gòu)包含另一個字段 amended,當 sync.Map 對象通過添加新鍵進行擴展并且自添加新鍵后尚未發(fā)生升級時,該字段為真。在這種情況下,鍵進入可寫的 map,而可讀 map 中沒有它的記錄。amended 字段為 true 時,表示這種特定狀態(tài),其中 dirty映射包含的記錄,但是 read 不包含記錄。
加載、存儲和刪除如何在高層次上工作?
這些操作的讀取部分通常是廉價的(相對)。原因是從 atomic.Value 字段中檢索到 readOnly 對象的指針,并快速搜索與鍵關(guān)聯(lián)的值。如果該值存在,則返回。
如果它不存在,那么它很可能是最近添加的。在這種情況下,需要檢查 dirty 字段(需要 mu 字段的保護)。如果鍵存在于 dirty 字段中,則將其作為結(jié)果進行檢索。
在此實現(xiàn)中(譯者注: dirty map),讀操作會比較慢。隨著 readOnly 對象中的每次未命中,字段 misses 的值自動遞增。當 misses 計數(shù)大于 dirty 的大小時,通過創(chuàng)建一個新的 readOnly對象來包含它,它會被提升(直接移動它的指針)到 read 字段。發(fā)生這種情況時,會丟棄 read 字段的先前值。
所有涉及 dirty map 的操作都在互斥鎖 mu 保護的區(qū)域內(nèi)執(zhí)行。
當記錄被存儲到 sync.Map 中時,它會通過以下三種方式之一進行處理:
1、修改 (read.Load().(readOnly))[key]返回的 entry 實例,如果檢索成功。修改是通過將值添加到 entry 字段來完成的。
2.修改 dirty[key] 返回的 entry 實例,如果檢索成功。修改是通過替換 entry 對象(存儲在 atomic.Value 類型的 read 對象內(nèi))中的值來完成的。如果第一步失敗,則執(zhí)行此操作。
3.如果 dirty 為 nil(當read.amended為false時就是這種情況),創(chuàng)建一個新的 map 并將只讀 map 中的所有內(nèi)容復(fù)制到它。然后將記錄添加到 dirty(注意,隨著字段被復(fù)制,entry 對象的地址就是被復(fù)制的對象。因此,讀映射和寫映射中存在的每個鍵都指向準確的相同的 entry 對象。)
在刪除的情況下,如果鍵存在于 read 中的 readOnly 對象中,它的 entry 實例會自動更新為 nil。這個操作不需要獲取 mu。但是在 dirty 字段中搜索鍵時需要獲取 mu。如果鍵不存在于 read 中,它會在 dirty 中查找,同時由 mu 保護。如果只在 dirty 中找到,則直接使用內(nèi)置的 delete 函數(shù)刪除 entry 對象。
因此,當在 read 字段持有的 readOnly 實例中找到鍵時,讀取、寫入和刪除操作將是完全原子的。而其他需要通過 dirty 處理的情況將由 mu 保護。
對 entry 字段所做的任何修改都將反映在 read 和 dirty map 中的 readOnly 對象上。因為,每當通過復(fù)制 readOnly 實例創(chuàng)建新的 map[interface{}]entry 對象時,只會復(fù)制 entry 對象的地址。
存儲 expunged 和簡單的 nil 之間的區(qū)別
始終認為以下屬性成立:
entry里面有一個指針。在任何時間,它的指針都可以保存expunged、nil或指向接口的指針,該指針保存了用戶通過Store(...)或LoadOrStore(...)方法提供的有效值。如果
entry保存的是nil,則表示與鍵關(guān)聯(lián)的值已通過調(diào)用Delete(...)或LoadAndDelete(...)進行刪除,而它存在于readOnly對象中,并且臟map。If
entryholds expunged, it signifies that a non-nildirtymap exists which does not have the same key that associates theentryfield.如果
entry包含expunged,則表示存在非nildirty映射,該映射不具有關(guān)聯(lián)entry字段的相同鍵。
當misses > len(dirty)(misses 是sync.Map 結(jié)構(gòu)中的一個字段)時,dirty map 被復(fù)制到 atomic.Value 類型的 read 字段中。這樣做的代碼是:m.read.Store(readOnly{m: m.dirty}) 其中,這里的 Store 是一個原子存儲操作。
readOnly 對象中有兩個字段。一個用于 map,另一個用于 amended 變量,該變量表示升級后是否將新鍵添加到 sync.Map 對象。第一次嘗試在升級后插入鍵會導(dǎo)致 readOnly對象內(nèi)的 map 被逐鍵復(fù)制到 dirty map 鍵。與存儲 nil 的 entry 對象相關(guān)聯(lián)的每個鍵都不會被復(fù)制到 dirty 映射中,而是將其 entry 對象更新為包含expunged。
因此,expunged 的鍵僅存在于 干凈 映射中,而不會將它們復(fù)制到新的 dirtymap。這是在假設(shè)不會再次添加被刪除一次的鍵的情況下完成的。
有兩個獨立的未導(dǎo)出方法,為 sync.Map 定義的 missLocked() 和 dirtyLocked()。他們的責(zé)任如下:
傳播 dirty映射(同時將sync.Map中的dirty字段設(shè)置為nil)將 readOnly對象(其amended字段設(shè)置為false)中的鍵值對復(fù)制到新創(chuàng)建的dirtymap 對象(通過忽略與entry對象關(guān)聯(lián)的鍵)設(shè)置為nil并使它們expunged;因為它們沒有被復(fù)制到dirtymap 中)。missLocked()每次在讀取readOnly對象時出現(xiàn)鍵未命中時都會被調(diào)用。除了Range(…)之外,sync.Map中定義的每個導(dǎo)出方法都可以觸發(fā)調(diào)用,因為它們都首先嘗試檢索存儲的記錄并接受key作為參數(shù)。missLocked()僅在其大小小于未命中數(shù)時才會提升dirty映射。
Store(key, value interface{}) 接口:
源代碼 6: Store 方法
// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
/// PART 1
read, _ := m.read.Load().(readOnly)
if e, ok := read.m[key]; ok && e.tryStore(&value) {
return
}
///------------------------------------------------------------
/// PART 2
m.mu.Lock()
read, _ = m.read.Load().(readOnly)
if e, ok := read.m[key]; ok {
if e.unexpungeLocked() {
// The entry was previously expunged, which implies that there is a
// non-nil dirty map and this entry is not in it.
m.dirty[key] = e
}
e.storeLocked(&value)
///--------------------------------------------------------------
/// PART 3
} else if e, ok := m.dirty[key]; ok {
e.storeLocked(&value)
/// -------------------------------------------------------------
/// PART 4
} else {
if !read.amended {
// We're adding the first new key to the dirty map.
// Make sure it is allocated and mark the read-only map as incomplete.
m.dirtyLocked()
m.read.Store(readOnly{m: read.m, amended: true})
}
m.dirty[key] = newEntry(value)
}
m.mu.Unlock()
/// --------------------------------------------------------------
}
讓我們將上面的代碼分成四部分(上面標記了包含每個部分的區(qū)域)。第一部分嘗試從 read 中檢索值,這是一個包含 readOnly 對象的 sync.Value 字段。如果讀取成功,它會嘗試將值以原子方式存儲到 entry對象中。源代碼 7 顯示了它是如何完成的。
tryStore(…)操作中的 atomic 更新包含一個無限循環(huán),該循環(huán)在值之前被 expunged 時終止。這是因為,expunged 值表示在 dirty map 中沒有相同鍵字段的副本,而在 只讀 對象中有一個。因此,在這種情況下更新指針不會反映在 dirty map 上,它總是應(yīng)該包含 sync.Map 的完全更新和準確狀態(tài)。除了最近剛剛推廣的情況。在這種情況下,dirty 字段將暫時保留 nil,直到第一次嘗試寫入它,因為它被設(shè)置為 nil。
源代碼 7 中的無限循環(huán)導(dǎo)致函數(shù)保持 “忙等待” 狀態(tài),直到更新成功。原子比較和交換操作接受三個參數(shù)。要修改的指針、指針中可能包含的舊值和新值。如果舊值與指針中保存的原始值相同,則新值存儲在指針中。
在源代碼 7 中,指針首先從 entry 對象中自動加載,并檢查它是否被 expunged。如果是,那么tryStore 會失敗,因為expunged 表示entry 對象不存在于與其鍵關(guān)聯(lián)的 dirty map 中。因此,將值存儲到從 read 映射中檢索到的 entry 對象將不再有用(因為修改不會反映在 dirty map 上)。
如果 e.p(存儲在 entry 對象中的指針)與之前的p值相同,則源代碼 7 第 11 行中的 atomic-compare-and-swap 指令負責(zé)添加新值在源代碼 7 的第 8 行讀取。如果在不同的 goroutine 中運行的方法在第 11 行執(zhí)行之前原子地修改了 p 的底層值,比較和交換指針操作失敗導(dǎo)致循環(huán)繼續(xù)。如果 p 被修改為包含 expunged,那么循環(huán)會因為第 8 行的條件分支而中斷。
這個無限循環(huán)將一直持續(xù)到在第 7 行到第 11 行的語句被執(zhí)行時 e.p 沒有被不同的 goroutine 修改。這就是為什么當我們使用 atomic 指令而不是讓 goroutine 休眠直到需要再次運行的鎖時,CPU 上的爭用會更加嚴重。原子指令導(dǎo)致 “忙等待” 發(fā)生,直到?jīng)]有爭用。
源代碼 7:tryStore 方法
// tryStore stores a value if the entry has not been expunged.
//
// If the entry is expunged, tryStore returns false and leaves the entry
// unchanged.
func (e *entry) tryStore(i *interface{}) bool {
for {
p := atomic.LoadPointer(&e.p)
if p == expunged {
return false
}
if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
return true
}
}
}
其余部分 2、3 和 4(在 Source 6 中)都在一個由 mu 鎖保護的區(qū)域內(nèi)運行。第 1 部分在 key 存在于 readOnly 對象中并且 tryStore 操作(如上所述)成功時返回。
但是第 1 部分失敗意味著 key 不存在于 readOnly 對象中,或者它已被刪除。繼續(xù)第 2 部分,讀取值再次重新加載到鎖定區(qū)域內(nèi)。
可能會在調(diào)用從不同 goroutine 執(zhí)行的 missLocked() 之后被替換,該 goroutine 也將在獲取 mu 后執(zhí)行(_Note ,每個帶有后綴 Locked 的函數(shù)都應(yīng)該在由 mu 保護的鎖定區(qū)域內(nèi)執(zhí)行)。但是因為第 1 部分沒有獲取 mu,在源代碼 6 的第 6 行檢索到的值可能會過時。
在第 2 部分中,再次檢索了 entry 指針?,F(xiàn)在,調(diào)用e.unexpungeLocked() 檢查存儲在entry中的值是否被 expunged:
如果它是 nil或其他任何東西,則表示在dirty映射中也必須存在相同的鍵。但如果它是 expunged,則表示該鍵不存在于dirty映射中,并且必須將其添加到其中。因此,由read.m[key]檢索到的entry對象的指針被存儲到與其適當?shù)逆I相關(guān)聯(lián)的dirty中。因為使用了相同的指針,對底層 entry 對象的任何更改都會反映在 “干凈” map 和 “dirty” map中。對unexpungeLocked()的調(diào)用會執(zhí)行語句return atomic.CompareAndSwapPointer(&e.p, expunged, nil)(這是其定義中唯一的語句)。這確保了e.p僅在它被expunged時更新。您不必在這里忙著等待以允許更新發(fā)生。這是因為CompareAndSwapPointer(...)的 “舊指針” 參數(shù)是一個常量(expunged)并且它永遠不會改變。
tryStore() 和 unexpungeLocked() 都可以更新 e.p,盡管它們不是由同一個互斥鎖相互保護的。因此,他們可能會嘗試從不同的 goroutines 同時更新 e.p。但這不會成為競爭條件,因為 unexpungeLocked() 應(yīng)該僅在其指針(入口 entry 的 p 字段)設(shè)置為 expunged 時修改入口 entry。
在不同的 goroutine 上運行的 unexpungeLocked() 可以在 Source 7 的第 7 行和第 11 行之間的任何地方執(zhí)行它的 CompareAndSwapPointer(…) 語句。如果它在這些行之間執(zhí)行,底層指針的值將在第 11 行的比較和交換操作將導(dǎo)致循環(huán)失敗并再次重復(fù)。因此,如果出現(xiàn)以下情況,則無法成功執(zhí)行 Source 7 中第 7 行和第 11 行之間的區(qū)域:
不同的 goroutine 執(zhí)行相同的區(qū)域,試圖修改相同的底層指針,或者,
如果unexpungeLocked() 在同一時間范圍內(nèi)同時執(zhí)行。在第 2 部分中,值最終通過調(diào)用
storeLocked(...)存儲到entry對象中?,F(xiàn)在轉(zhuǎn)到第 3 部分。第 2 部分中的條件在兩種可能性下失敗(注意,我們已經(jīng)排除了鍵可能存在于readOnly對象中的可能性):鍵存在于
dirtymap 中。鍵不在
dirtymap 中。第 3 部分處理第一種情況。它只是將記錄存儲到 entry 對象中?,F(xiàn)在第 4 部分處理以下兩種可能性:dirtymap 是nildirtymap 不是nil如果dirtymap 是nil,則必須在添加新鍵之前通過從readOnly對象復(fù)制entry來創(chuàng)建它。否則,直接添加鍵而不做任何更改。
dirty 映射為nil 也表示自從它的 dirty map 被提升為“干凈” map 以來,沒有新entry被添加到 sync.Map 對象中。因此,如果 dirty 是 nil,則源 5 中的字段 read.amended應(yīng)該是 false。
在將 dirty map 提升為干凈 map 時(這發(fā)生在對結(jié)構(gòu) sync.Map 中定義的 missLocked()的調(diào)用時;它僅在Load(...)、LoadOrStore(...)時執(zhí)行、LoadAndDelete(...) 或 Delete(...) 被調(diào)用)它只是被直接復(fù)制到“干凈”的 map 中。發(fā)生這種情況時,用戶可以自由地同時刪除干凈map中的鍵(這只是一個原子操作)并重新添加它們。但是在升級后第一次嘗試將新鍵添加到map中時,干凈映射的內(nèi)容被復(fù)制到臟映射中。但是當發(fā)生這種情況時,在其 entry 對象的指針字段中帶有 nil 的鍵被忽略,并且在“干凈” map 中,它們被原子地更改為expunged。
在第 4 部分中,調(diào)用了 dirtyLocked() 來填充 dirty 映射。來源 [dirtyLocked()](https://github.com/golang/go/blob/aa4e0f528e1e018e2847decb549cfc5ac07ecf20/src/sync/map.go#L362)
源碼 8:dirtyLocked() — 通過復(fù)制可讀 map 的內(nèi)容創(chuàng)建一個新 map 并將其存儲在
dirty中
func (m *Map) dirtyLocked() {
if m.dirty != nil {
return
}
read, _ := m.read.Load().(readOnly)
m.dirty = make(map[interface{}]*entry, len(read.m))
for k, e := range read.m {
if !e.tryExpungeLocked() {
m.dirty[k] = e
}
}
}
tryExpungeLocked() 定義如下:
源碼 9:
tryExpungeLocked()——如果它是nil,則用 expunged 更新entry的指針
func (e *entry) tryExpungeLocked() (isExpunged bool) {
p := atomic.LoadPointer(&e.p)
for p == nil {
if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
return true
}
p = atomic.LoadPointer(&e.p)
}
return p == expunged
}
像源代碼 7 中定義的 tryStore(...) 一樣,源代碼 8 也依賴于一個完全原子的操作來將 expunged 設(shè)置為指針。正如我們在 Source 8 中看到的,新的 dirty map 的創(chuàng)建和最終更新僅在 dirty 為 nil 時發(fā)生。源代碼 8 的第 9 行清楚地表明,只有當 tryExpungeLocked() 失敗時,才會將鍵添加到 dirty map中。
tryExpungeLocked() 確保 entry對象中的指針字段設(shè)置為 expunged,如果它最初是 nil(參見源代碼 9 的第 4 行)。如果在比較和交換操作之前修改了指針,則交換失敗并且循環(huán)退出。這個循環(huán)一直持續(xù)到 p 不是 nil。這可能會在執(zhí)行比較和交換之前從 nil 改變;例如,它可以被一個 goroutine 替換,該 goroutine 執(zhí)行對 sync.Map 結(jié)構(gòu)中的 Store(...) 方法的調(diào)用。
在調(diào)用 dirtyLocked() 之后,讀取的map被標記為“已修改”。這表示 read 字段中的map已經(jīng)失效。
Load(key interface{}) (value interface{}, ok bool):
注意:如果您還沒有閱讀Store(...) 部分,我建議您閱讀。我在那里使用的推理路線也適用于這里以及下面解釋的所有其他方法。因此我不會在這里重新解釋它們。
源代碼 10:Load
// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
read, _ := m.read.Load().(readOnly)
e, ok := read.m[key]
if !ok && read.amended {
m.mu.Lock()
// Avoid reporting a spurious miss if m.dirty got promoted while we were
// blocked on m.mu. (If further loads of the same key will not miss, it's
// not worth copying the dirty map for this key.)
read, _ = m.read.Load().(readOnly)
e, ok = read.m[key]
if !ok && read.amended {
e, ok = m.dirty[key]
// Regardless of whether the entry was present, record a miss: this key
// will take the slow path until the dirty map is promoted to the read
// map.
m.missLocked()
}
m.mu.Unlock()
}
if !ok {
return nil, false
}
return e.load()
}
以原子方式檢索 readOnly 對象。 檢查鍵及其 entry 是否存在于其中 如果成功,返回結(jié)果。此時,沒有使用鎖。 如果失敗,并且read.amended 為真(表示dirty 字段不是nil),則從可寫的 map(即dirty 映射)中讀取該值。 第 4 步在一個由 mu 保護的鎖定區(qū)域內(nèi)運行。源代碼 10 中的第 5、6 和 7 行在第 12、13 和 14 行中再次重復(fù);但這是在鎖定區(qū)域內(nèi)完成的。這樣做是因為,在此期間,與該鍵關(guān)聯(lián)的不同 goroutine 可能添加了一個新的 entry 對象。但是所有的寫操作都與鎖同步。因此,這里不會有任何競爭條件。 如果通過可讀的 map 檢索失敗,則從可寫的 map 中檢索。
LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
LoadOrStore(…) 的源代碼點擊這里.
在大多數(shù)情況下,LoadOrStore(...) 與 Load(...) 類似,除了它使用 e.tryLoadOrStore(value)代替e.tryLoad() 并且還調(diào)用了 missLocked( ...) 如果鍵在可讀的map中不存在。
tryLoadOrStore(...) 的實現(xiàn)類似于使用比較和交換指令的任何原子更新策略。只有當 entry 對象持有除 expunged(包括 nil)以外的任何指針時,此方法才會成功。
LoadAndDelete(key interface{}) (value interface{}, loaded bool):
跟隨的策略 與上面提到的策略并不是很獨特。第一次讀取嘗試使用 readOnly 對象。如果成功,e.delete() 被調(diào)用,它以原子方式將 entry 對象的指針設(shè)置為 nil。如果失敗,則從鎖定區(qū)域內(nèi)的 dirty map 中檢索 entry 對象。如果 dirty 映射有鍵,就會調(diào)用 e.delete()。The key is not removed at this point. It is just set to nil. 此時未移除鍵。它只是設(shè)置為 nil。
Delete()(value interface{}, ok bool):
內(nèi)部調(diào)用 LoadAndDelete(…)
Range(f func(key, value interface{}) bool)
首先嘗試從 readOnly對象中檢索密鑰如果成功,則立即使用返回的對象計算范圍循環(huán)(從 源碼) 中的第 341 行到 349 行)。 如果失敗,則在 line 325 處檢查, 查看 sync.Map對象是否通過在最近的dirty map 升級后使用附加鍵對其進行擴展來 “修改”。如果在line 325處檢查成功,則立即提升 dirtymap。這樣做是因為,假設(shè)訪問了所有鍵,范圍操作最有可能是 O(N)。因此,在提升dirty映射并將dirty設(shè)置為nil之后,我們可以期待接下來的存儲操作(引入新鍵的Store(...)或LoadOrStore(...))遵循創(chuàng)建新的dirtymap 的緩慢路徑,這是一個 O(N) 操作。但是調(diào)用Range(...)本身就是一個 O(N) 操作,在 O(N) 操作創(chuàng)建一個新的dirtymap 之前確保 O(N) 操作(創(chuàng)建一個新的dirtymap)只跟在另一個 O(N) 操作之后。因此我們可以將它們攤銷為一個 O(N) 操作在步驟 4 之后,執(zhí)行范圍操作。
sync.Map 和 RWMutex 保護的 map 之間的性能對比??焖賰?yōu)化指南
這篇文章(go 中的新成員 -- sync.Map by Ralph Caraveo III) 詳細說明了當我們僅使用幾個核心處理器時,簡單的 sync.RWMutex保護 map 如何比 sync.Map 性能更好。文章中的基準測試表明,超過 4 個內(nèi)核,sync.Map 的性能似乎明顯高于由 sync.RWMutex保護的 map。
因此,有必要構(gòu)建一個原型,稍后可以對其進行基準測試,以測試其實現(xiàn)的每個變體的相對性能。這可以幫助我們選擇最適合每種情況的變體。這與深度神經(jīng)網(wǎng)絡(luò)的工作方式有相似之處。在 DNN 中,我們有一個損失函數(shù),我們試圖將其值最小化。這種情況下的損失函數(shù)表達了神經(jīng)網(wǎng)絡(luò)模型的目標。
同樣,應(yīng)用程序必須將它們的測試用例和基準一起視為表達整個項目目標的 “損失函數(shù)”。如果你衡量績效,及時你可以改進它。因為,你無法調(diào)整你沒有衡量的東西。
當您優(yōu)化的內(nèi)容沒有提高應(yīng)用程序的整體性能,或者性能的變化太小可以忽略不計時,過早的優(yōu)化確實會成為一個問題。
讓我們看一個例子:
假設(shè)您有一個應(yīng)用程序,它應(yīng)該通過 API 調(diào)用接收配置文件信息以及特定的元數(shù)據(jù),將其轉(zhuǎn)換為不同的格式并將它們發(fā)送到不同的微服務(wù)。假設(shè)您編寫了一個過程來過濾正在讀取的數(shù)據(jù)中的特定模式 - 假設(shè)您收到客戶資料數(shù)據(jù),并且您的應(yīng)用程序應(yīng)該過濾超過 8 年的資料并將其 ID 發(fā)送到不同的消息隊列,同時還存儲緩存中的配置文件。
你應(yīng)該使用sync.Map 還是使用 sync.RWMutex 保護的 map?
假設(shè)在應(yīng)用程序收到的每 100 個配置文件數(shù)據(jù)中,其中一個是 8 年前的。那么你真的應(yīng)該考慮使用哪種類型的 map 嗎?這些選擇中的任何一個在這里都沒有區(qū)別,因為您的選擇通常不會損害系統(tǒng)的整體性能。也許是因為這個緩存不是最慢的一步。
當我們以一個團隊的方式工作時,讓不同的人處理不同的任務(wù)很常見。因此,讓人們通過使用本地化基準測試使他們的代碼部分運行得更快,您不會從中受益。基準測試必須反映應(yīng)用程序的總體目標。
如果它不是真正的瓶頸,那么任何用于提高部分代碼庫性能的工作都將被浪費掉。但是當您編寫一個大多數(shù)功能都向用戶公開的庫時,情況就有些不同了。在應(yīng)用程序級別(包括公開的 API)編寫基準測試有助于設(shè)定團隊希望實現(xiàn)的目標。
如果閱讀這篇文章給你的印象是 Go 作者過早地優(yōu)化,我不得不說,那不是真的。Go 是一種通用語言,我相信即使是它的作者也無法完全預(yù)測它的所有用例。但是他們總是有可能找出最常見的用例并為此進行優(yōu)化。他們的首要任務(wù)應(yīng)該是調(diào)整對其生態(tài)系統(tǒng)影響最大的部分。為此,他們絕對需要來自所有用戶的非常嚴格的反饋循環(huán)。他們的主要重點應(yīng)該是解決痛點。
極致的優(yōu)化是什么樣的?由于我之前提到的原因,實際上并不需要將應(yīng)用程序中的性能提高到某一點。也就是說,如果您確實希望出于某種原因使您的應(yīng)用程序超快,那么請繼續(xù)閱讀!
SQL 數(shù)據(jù)庫系統(tǒng)竭盡全力使事情運行得更快。他們通過為同一任務(wù)實施多個 “策略” 來做到這一點。當您查詢未編入索引的記錄時,您將觀察到最壞情況的性能。當查詢引擎回退到使用暴力搜索時,就會發(fā)生這種情況。
但是如果您建立了索引,查詢引擎就有機會選擇更好的算法(僅引用索引)來提高查詢執(zhí)行時間。“查詢計劃” 階段負責(zé)確定要使用的正確策略。如果您構(gòu)建了多個索引,那么您實際上是在為查詢引擎提供太多可供選擇的選項和策略。在這種情況下,它甚至可能依靠執(zhí)行統(tǒng)計來選擇最佳策略以供內(nèi)部使用。
同樣,如果您想要一個 super fast 并發(fā) map,以下內(nèi)容可能會有所幫助:
源代碼 11:超高效同步 map
import (
"sync"
)
// RWMutexMap is an implementation of mapInterface using a sync.RWMutex.
type RWMutexMap struct {
mu sync.RWMutex
dirty map[interface{}]interface{}
}
func (m *RWMutexMap) Load(key interface{}) (value interface{}, ok bool) {
m.mu.RLock()
value, ok = m.dirty[key]
m.mu.RUnlock()
return
}
func (m *RWMutexMap) Store(key, value interface{}) {
m.mu.Lock()
if m.dirty == nil {
m.dirty = make(map[interface{}]interface{})
}
m.dirty[key] = value
m.mu.Unlock()
}
func (m *RWMutexMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
m.mu.Lock()
actual, loaded = m.dirty[key]
if !loaded {
actual = value
if m.dirty == nil {
m.dirty = make(map[interface{}]interface{})
}
m.dirty[key] = value
}
m.mu.Unlock()
return actual, loaded
}
func (m *RWMutexMap) LoadAndDelete(key interface{}) (value interface{}, loaded bool) {
m.mu.Lock()
value, loaded = m.dirty[key]
if !loaded {
m.mu.Unlock()
return nil, false
}
delete(m.dirty, key)
m.mu.Unlock()
return value, loaded
}
func (m *RWMutexMap) Delete(key interface{}) {
m.mu.Lock()
delete(m.dirty, key)
m.mu.Unlock()
}
func (m *RWMutexMap) Range(f func(key, value interface{}) (shouldContinue bool)) {
m.mu.RLock()
keys := make([]interface{}, 0, len(m.dirty))
for k := range m.dirty {
keys = append(keys, k)
}
m.mu.RUnlock()
for _, k := range keys {
v, ok := m.Load(k)
if !ok {
continue
}
if !f(k, v) {
break
}
}
}
// MapInterface is the interface Map implements.
type MapInterface interface {
Load(interface{}) (interface{}, bool)
Store(key, value interface{})
LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
LoadAndDelete(key interface{}) (value interface{}, loaded bool)
Delete(interface{})
Range(func(key, value interface{}) (shouldContinue bool))
}
func NewSuperEfficientSyncMap(numOfCores int) MapInterface {
if numOfCores == 0 {
numOfCores = GOMAXPROCS(0)
}
// Or this could include more complex logic with multiple
// strategies.
if numOfCores > 4 {
return sync.Map{}
}
return RWMutexMap{}
}
策略模式幾乎可以在任何地方使用,讓應(yīng)用程序的不同部分可以對其性能進行巨大的控制。您還可以編寫模塊來記下統(tǒng)計信息并為您的應(yīng)用程序設(shè)計正確的策略,更像是查詢計劃器的工作方式。
這樣,您的應(yīng)用程序?qū)槊總€環(huán)境使用一組不同的策略!如果您的客戶關(guān)心性能并且您支持廣泛的平臺,這將非常有用。或者,如果您正在構(gòu)建一種語言,它將最有用。
也就是說,如果您清楚團隊的目標,總是可以避免過早優(yōu)化。如果沒有嚴格的目標,您就無法真正知道什么是 “優(yōu)化”。
原文地址:https://github.blog/2021-07-22-github-supply-chain-security-features-go-community/
原文作者:William Bartholomew
本文永久鏈接:https://github.com/gocn/translator/blob/master/2021/w28_GitHub_brings_supply_chain_security_features_to_the_Go_community.md
譯者:朱亞光
