回顧和今天的計劃
昨天的進展令人驚喜,原本的調試系統已經被一個新的系統完全替換,新系統不僅能完成原有的所有功能,還能捕獲完整的調試信息,包括時間戳等關鍵數據。這次的替換非常順利,效果很好。
今天的重點是在此基礎上繼續深入研究,因為雖然數據已經成功采集,但還沒有真正利用這些數據。因此,今天的目標是分析目前的系統,找出可能存在的缺陷,并嘗試解決這些問題。如果時間充足,還會探索如何更好地使用這些數據。
目前有一個小問題需要解決,這是一個近期在實際開發中較少遇到的問題。由于代碼結構的不同,之前的項目并不需要解決這一問題,但在當前的代碼中,這個問題必須被解決。雖然已經有一個可能的解決方案,但仍需進一步驗證其是否合適。
接下來的步驟包括:
- 分析當前系統的不足之處——確定哪些問題需要解決,以及哪些問題可以暫時忽略。
- 優化數據的可視化——如果時間允許,將開始改善數據的展示方式,使其更加直觀。
- 改進性能分析工具——通過增強數據的可視化,進一步優化當前的性能分析系統。
數據的可視化改進將重點關注幾個方面,例如:
- 計算函數的獨占執行時間:區分一個函數本身的執行時間和它調用的其他函數的執行時間,從而獲得更準確的性能分析數據。
- 事件發生的相對時間:分析不同事件之間的時間關系,以便更好地理解系統的執行過程。
最終目標是打造一個高效的內聯性能分析系統。實際經驗表明,開發這樣一個系統并不需要太長時間,在幾周的開發時間內,即便是非全職的工作量,也可以構建出一個功能完整、易用的性能分析工具。擁有這樣的系統,可以大幅提升代碼優化和調試的效率。
關于擁有一個優秀的內聯性能分析系統的好處α
在開發過程中,始終保持對程序運行情況的了解是非常重要的。內聯分析工具(inline profiler)能夠幫助我們隨時掌握代碼的執行情況,而無需切換到“現在我要進行性能分析”的模式。相比于啟動外部分析工具,內聯分析工具的優勢在于,它能夠持續提供代碼的執行信息,而不需要額外的步驟或中斷開發流程。
即使在某些情況下仍然需要外部分析工具,比如它們具備某些內聯工具不具備的功能,但內聯工具依然是一個很好的補充。它的核心作用在于保持對代碼的“態勢感知”(situational awareness),使我們能夠隨時了解代碼的運行狀態。這不僅有助于優化性能,還可以更早地發現bug或異常行為,避免到了項目后期才發現嚴重問題。
如果在開發過程中沒有持續關注代碼的執行情況,那么最終可能會陷入“打開汽車后備箱,發現里面塞滿了尸體”的尷尬局面——也就是說,直到項目后期才意識到代碼存在嚴重的性能或結構問題,這會讓解決問題變得更加困難。因此,始終掌握代碼的執行情況是一個明智的選擇,它可以幫助我們在開發過程中做出更好的架構決策,避免在項目后期被性能問題困擾,陷入“我該怎么辦?”或者“我是不是把自己逼入了死角?”的困境。
構建一個基本的內聯分析系統并不需要太多時間。通常只需要花費十幾個小時,最多幾十個小時,就可以從零搭建一個相當完善的系統。相較于整個項目的開發周期,這種投入的成本是非常低的,但帶來的收益卻是巨大的。因此,建議在自己的項目中保持這種做法,讓代碼的執行情況始終處于可監控的狀態。
如果要跟隨當前的代碼版本進行學習,可以獲取相應的源代碼。例如,可以使用GitHub的標簽功能找到特定版本的代碼,也可以通過其他方式下載源代碼。無論使用哪種方式,確保獲取到正確的代碼版本,就可以同步跟進開發進度。
性能分析系統只有在數據整理階段會變慢,這是可以接受的
在上一次的進展中,我們剛剛完成了一個新的系統,并且確認它能夠正常運行。整體來看,它的表現還算不錯,雖然比舊系統稍微慢了一些,但總體而言依然能夠接受。我們目前無法完全確定導致性能下降的具體原因,但直覺上認為這并不是由于插入了計時代碼導致的。我們的計時代碼應該仍然相當高效,而大部分額外的執行時間很可能是消耗在數據整理(collation)階段。
這種情況并不是什么大問題。即使數據整理階段的計算量較大,從而影響了垂直分析系統(vertical profile system)的運行速度,這也只是意味著在進行性能分析時,我們無法以完整的幀率運行游戲。然而,我們仍然可以采用其他方式來規避這個問題,比如以完整的幀率運行游戲,在需要查看分析結果時暫停游戲,或者使用其他類似的方法。因此,只要主要的開銷集中在數據整理階段,而不會影響關鍵的游戲代碼邏輯,那就不會造成太大的困擾。
接下來的任務就是深入研究這個問題,看看是否真的如我們所預想的那樣,性能開銷主要集中在數據整理階段。我們的內聯分析工具如果影響到了游戲的實際運行代碼,那才會構成真正的問題。但是,如果這些額外的計算都被隔離到了一個后處理階段(post pass)中,就不會對游戲的實時執行造成影響。從目前的實現來看,我們已經做了合理的優化,使得所有的額外計算都被放在后處理階段,因此理論上這不會影響游戲的正常運行。
只要額外的性能開銷可以被限制在后處理階段,就有很多方法可以解決它,而不會影響我們的使用體驗。這種情況并不妨礙我們利用這個工具進行分析,因此目前來看,不需要過于擔心性能問題。接下來的工作重點是繼續深入優化,并確保它不會對關鍵代碼路徑造成干擾。
當前性能分析系統的局限性:
接下來,我們的目標是討論當前緩沖區的工作方式以及它們的一些限制。首先,有一些限制可能不會真正影響我們,也可能永遠不需要修復。盡管如此,我們仍然需要明確這些限制的存在,并在代碼中標注“TODO”,以便記錄這一點并進行討論。
其中一個值得注意的限制是,目前我們并沒有采取任何措施來確保最終的調試記錄在寫入事件數組后,能夠在事件數組索引交換之前完整地輸出。換句話說,在進行事件數組的輪換或索引交換時,可能會存在部分調試記錄尚未完全寫入的情況。
這意味著我們的調試系統依賴于全局事件數組,但在索引切換時,可能會導致部分數據的丟失或未完成的寫入。這并不會對大多數情況下的調試工作造成影響,但仍然是一個潛在的問題。如果有必要,我們可以在未來考慮一些同步機制,確保調試記錄在數組切換前完成寫入。然而,目前來看,這并不是一個必須要解決的問題。
1) 在極端罕見的情況下,分析器可能會讀取錯誤數據
當前的調試系統使用了調試事件數組(debug event array),并且采用了**雙緩沖(double buffering)**機制。這樣做的主要目的是為了支持多個線程的并發寫入,并確保調試數據可以持續記錄,而不會因為數據讀取而阻塞寫入進程。
由于事件流是從多個線程寫入到調試日志中的,因此我們不能簡單地等到緩沖區填滿后再清空它并從頭開始寫入。必須保證在讀取完所有數據之前,不會覆蓋已有的數據。然而,同時還需要讓其他線程能夠繼續寫入數據,并且不能因為調試記錄的機制導致額外的性能損耗。畢竟,計時代碼的設計目標就是盡可能高效,直接寫入調試日志,而不影響主程序的執行。
雙緩沖的工作原理
為了實現這一點,系統使用了兩個緩沖區:
- 在一個緩沖區中寫入調試事件;
- 另一個緩沖區用于讀取和處理這些事件。
當需要交換緩沖區時,會使用**原子操作(atomic swap)**來切換當前的寫入緩沖區和讀取緩沖區,從而確保不會發生競爭條件(race condition)。這樣可以保證:
- 線程可以持續地寫入調試事件,而不會因為讀取操作而被阻塞;
- 讀取線程可以完整地獲取上一個緩沖區的數據,而不會受到新數據寫入的影響。
潛在的問題
理論上,存在一種極端情況下可能發生數據錯誤的情況:
- 某個線程獲取了當前調試事件數組的索引,但尚未寫入數據;
- 在它執行寫入之前,該線程被操作系統搶占(preempted),即被掛起;
- 這期間,另一個線程完成了調試事件數組的原子交換(atomic swap),并開始解析新緩沖區的數據;
- 如果這個解析操作一直進行到緩沖區末尾,可能會讀取到尚未被寫入的數據,從而導致錯誤。
然而,這種情況的發生概率極低,原因如下:
- 事件數組的大小非常大,通常包含數十萬條記錄(目前已有超過64,000條),因此在緩沖區完全填滿、交換、解析的整個過程中,特定線程剛好在關鍵時刻被搶占的概率極低。
- 即便偶然發生了讀取無效數據的情況,它也僅影響調試信息,而不會影響程序的正常運行。因此,這個問題可以忽略不計。
為什么不修復這個問題?
- 修復成本高:要徹底解決這個問題,可能需要引入額外的同步機制,例如顯式地確保某個事件在緩沖區交換之前完成寫入。然而,這樣會影響計時代碼的性能,導致不必要的開銷。
- 影響極小:考慮到事件數組的大小以及極低的發生概率,這個問題基本上不會影響實際的調試工作。因此,沒有必要為了修復這個罕見的極端情況而引入復雜的同步機制。
最終決定
綜合考慮后,我們決定接受這一限制,因為它帶來的影響微乎其微,同時修復它的成本過高。盡管存在理論上的潛在風險,但考慮到發生概率極低,我們認為這是可以接受的折衷方案。當然,如果未來實際使用中發現它真的引發了問題,我們再考慮更進一步的優化方案。
現在,我們的重點將轉向那些確實需要修復的限制,以進一步完善調試系統。
2) 調試記錄沒有標記核心和線程索引
接下來需要關注的是一些尚未完成的部分,并不是系統本身存在問題,而是某些信息缺失,導致在調試時無法獲得足夠的上下文信息。
當前存在的問題
-
無法確定當前正在處理的核心(CPU Core)
在查看調試數據時,無法知道當前的事件是在哪個 CPU 核心上執行的。這意味著,在分析多線程任務的調度情況時,缺少了關鍵的信息,不利于判斷性能瓶頸或線程競爭問題。 -
無法確定當前的線程索引(Thread Index)
目前沒有記錄每個事件所屬的線程索引,導致在調試時無法確定是哪個線程生成了特定的事件。這對于多線程環境下的調試來說是一個較大的缺陷,因為不同線程的執行順序可能會影響程序的正確性和性能。
問題的影響
由于缺少這些關鍵的標識信息,在調試時會面臨以下問題:
- 無法區分不同線程的執行情況
例如,如果某個事件在某個線程中發生了性能問題,目前的調試系統無法直接提供信息來確定問題發生在哪個線程上,需要額外的手段來推斷。 - 無法分析 CPU 核心的任務分配情況
在多核 CPU 環境下,任務可能會被分配到不同的核心執行。如果無法得知具體的核心編號,就無法分析任務的調度策略是否合理,或者是否存在線程遷移(Thread Migration)等影響性能的問題。
下一步改進方向
為了彌補這些信息的缺失,需要在調試事件系統中加入:
- 核心標識(Core ID):記錄當前事件是在哪個 CPU 核心上執行的,以便分析任務的調度情況。
- 線程索引(Thread Index):為每個線程分配唯一的索引,并在調試記錄中存儲該信息,以便能夠區分不同線程的執行情況。
這些改進將有助于提高調試系統的可用性,使其能夠更準確地反映多線程和多核環境下的程序運行狀態,從而更有效地進行性能分析和錯誤排查。
核心索引并不那么重要……
目前,由于缺少關鍵信息,導致實際調試工作受到一定限制。特別是在當前狀態下,無法確定核心索引(Core Index)和線程索引(Thread Index),這對調試多線程程序帶來了一定的不便。
核心索引(Core Index)
- 這個信息屬于**“錦上添花”**的類別,主要用于分析線程在不同 CPU 核心上的運行情況,以及它們是否被調度系統遷移(Thread Migration)。
- 如果能夠獲取該信息,在進行性能分析時,可以更直觀地觀察不同核心的負載情況。
- 但是,即使核心索引一直保持為 0,也不會對調試的主要工作產生實質性的影響,因此這個問題并不緊急。
獲取核心索引的嘗試
- 原本希望可以通過 ARDI TSC(時間戳計數器,Time Stamp Counter) 來獲取核心索引信息,但目前來看,這可能不是一個可行的方案。
- 未來或許可以找到其他方式來獲取該信息,但目前沒有明確的解決方案,因此暫時擱置。
最終決定
- 核心索引(Core Index)的問題并不緊急,可以忽略,即便一直保持為 0 也不會影響主要的調試工作。
- 未來可能會繼續研究如何更好地獲取這個信息,但目前不會優先解決這個問題。
……但線程索引卻至關重要
在多線程環境中,線程索引(Thread Index)是非常關鍵的,特別是在性能分析和調試中。線程索引之所以至關重要,是因為多個線程可能會調用相同的函數,這帶來了一些復雜性,尤其是在對渲染等關鍵時間敏感組件進行性能分析時。
問題的關鍵
當我們進行渲染函數的性能分析時,我們往往需要在渲染代碼中插入性能計數器(profiling counters)。然而,問題在于 多個線程可能同時調用相同的函數,這使得我們無法簡單地假設某個函數的開始和結束是在同一個線程中完成的。例如:
- 我們可能會看到一個“開始事件”(begin event),但這個事件的結束(end event)卻可能是由 另一個線程 完成的。
- 這種情況使得我們無法確定是哪個線程實際完成了該函數的執行,導致我們不能準確地將開始和結束事件匹配起來。
為什么線程索引重要
為了確保準確地進行事件配對(比如將開始和結束事件配對),我們需要一種方式來跟蹤每個線程的執行情況,特別是線程的執行順序。在多線程環境中,線程的執行順序是不確定的,可能會在不同的時間開始和結束同一個函數的執行,因此無法簡單地通過事件順序來判斷它們是否來自同一個線程。
如何解決這個問題
-
線程內的執行順序是確定的:在一個線程內,事件總是按照順序發生的——首先是開始事件(begin event),然后是結束事件(end event)。因此,如果所有的事件都來自同一個線程,我們可以確保這些事件是成對出現的,即一個“開始”事件對應一個“結束”事件。
-
多線程時的挑戰:但當多個線程同時執行時,情況就變得復雜。不同線程的開始和結束事件可能在任意時刻發生,這就導致了我們無法直接通過時間順序將它們配對。我們需要一個 唯一的標識符 來區分每個線程的事件,只有這樣才能確保我們能夠正確地將“開始”事件和“結束”事件配對起來。
解決方案
為了能夠正確地配對每個線程的開始和結束事件,我們必須確保:
- 每個線程都有一個唯一的標識符(例如線程索引),這樣我們可以知道哪個線程發出了某個事件。
- 通過這個唯一標識符,我們能夠跟蹤每個線程的執行路徑,并確保配對開始和結束事件時不會混淆不同線程的事件。
總結
線程索引是進行多線程調試和性能分析的關鍵,特別是在多個線程調用相同函數的情況下。為了確保準確的事件配對,必須依賴線程索引來標識每個線程的活動,并通過唯一標識符來避免線程間事件的混淆。這將幫助我們更精確地進行性能分析和調試工作。
解決線程區分問題的兩個選項:
針對多線程環境下事件配對的問題,解決方案有兩個主要的選擇:
1) 獲取線程索引
解決這個問題時,第一種方法是通過獲取線程索引來為每個線程分配一個唯一的標識符。通過這種方式,可以將每個線程的執行與其對應的事件關聯起來,從而正確配對開始和結束事件。這個線程索引不僅有助于準確配對事件,還能提供更多的額外信息,比如哪些線程在執行哪些操作,幫助我們更好地了解線程的行為。
這種方法的優勢在于,它能提供 更詳細的日志信息,不僅能夠識別出線程執行的具體操作,還能為后期的調試和性能分析提供更多有價值的上下文。這對于需要追蹤和分析線程行為的應用程序非常有用。
盡管如此,并不是所有的程序都需要過度依賴線程。在該項目中,線程數量并不多,主要包括一些資產線程、渲染線程和后臺合成線程等。并且,程序并不是以多線程為核心特性,線程并非程序的主要關注點。因此,如果認為為每個線程分配唯一標識符的工作量過大,可以選擇不這么做,這并不會對整體設計造成致命影響。這種額外的信息雖然有助于分析和調試,但并不是絕對必須的。
2) 使用原子操作生成唯一 ID,以匹配我們的起始和結束計時塊
另一個解決方案是,通過引入一個額外的計數器來替代線程索引。這個計數器是一個32位整數,每次被原子操作地遞增。我們不再使用線程索引,而是使用這個遞增的唯一ID。具體的做法是,在每個事件的開始塊時,我們同步地遞增該計數器,并將當前值插入到事件數據中;然后在結束塊時,使用相同的計數器值來確保開始和結束事件的配對。
由于我們知道,每次遞增都會產生一個唯一的ID,且這個計數器的最大值是40億,顯然在一個幀內不會達到這個值,因此即使計數器遞增到最大值,它也不會影響當前的調試輸出,因為計數器會在這個范圍內自動回繞。而且,即使它回繞,也不會對事件的正確配對產生問題,因為在調試中,每個ID都會是唯一的,并且能夠正確配對事件的開始和結束。
這種方式提供了基本的配對功能,確保每個開始事件和結束事件能夠正確地匹配。雖然這是一個較簡單的方法,但它能夠快速實現核心功能,確保調試事件的配對,并且在實際使用中很少會遇到ID溢出的問題。因此,這種方法是可行的,如果需要,完全可以立即實施并開始使用。
我們能用一個簡單的函數調用獲取線程索引嗎?
現在的主要問題是,是否能夠更容易地獲取與線程索引對應的數字。為了解決這個問題,首先要討論的是 Windows 環境下是否可以通過某個函數調用來獲取線程 ID。
在 Windows 中,有一個函數 GetCurrentThreadId()
可以用來獲取當前線程的 ID。不過,函數調用通常不適合在定時器塊(timer block)中執行,因為這可能會帶來性能上的問題。因此,在這種情況下,首先需要查看調用 GetCurrentThreadId()
函數會發生什么,確保它的使用不會導致不必要的復雜性或性能下降。
為了避免在實現過程中出現不必要的問題,決定將這個調用放在 Windows 特定的層次里,確保不會在不適合的環境中啟動,如啟動過程(booting)等。這樣做的目的是簡化事情,避免在不同的環境下造成復雜性,確保代碼能更方便地運行。
GetCurrentThreadId 是如何工作的?
首先,想要了解調用 GetCurrentThreadId()
函數實際會做什么,目的是查看其執行過程中的性能表現。為了觀察這一點,首先在 main
函數中設置斷點,然后啟動調試器,查看匯編代碼,并分析調用 GetCurrentThreadId()
時的開銷。
在調試過程中,查看了匯編代碼,發現獲取線程 ID 的過程非常直接。它通過訪問線程特定的內存段(通常是通過 gs
寄存器來引用的)來獲取當前線程的 ID。具體來說,線程 ID 是通過 gs
寄存器來進行尋址,加載到一個寄存器中,然后返回該值。
從這個分析可以看出,獲取線程 ID 的過程非常簡單,實際上它只是從線程本地存儲(TLS)中讀取一個指針,然后通過該指針來獲得線程 ID。因此,這個操作在性能上非常高效,不會引起太大的開銷。
單步進入
GS
寄存器是 x86 和 x86-64 架構中的一個段寄存器,主要用于存儲特定線程或進程的線程局部存儲(TLS)地址。它是 CPU 用于訪問線程局部數據的一部分,特別是在多線程環境中,GS
寄存器通常指向與當前線程相關的內存區域。
主要作用:
-
線程局部存儲(TLS):
- 在多線程程序中,每個線程都有一塊獨立的內存區域來存儲該線程的局部數據,這塊內存區域通常被稱為線程局部存儲(TLS)。為了訪問這些數據,每個線程的
GS
寄存器會指向該線程的 TLS 區域。
- 在多線程程序中,每個線程都有一塊獨立的內存區域來存儲該線程的局部數據,這塊內存區域通常被稱為線程局部存儲(TLS)。為了訪問這些數據,每個線程的
-
訪問線程特定的數據:
- 在 32 位系統中,
GS
被用來指向與線程相關的內存區域,而在 64 位系統中,GS
寄存器的用途更為廣泛,可以存儲對線程局部數據的指針(例如線程 ID、線程棧等)。
- 在 32 位系統中,
-
操作系統層面的使用:
- 操作系統通過使用
GS
寄存器來管理不同線程的狀態和信息。對于應用程序來說,可以通過訪問GS
指向的內存區域來獲取線程的相關數據,如線程的 ID。
- 操作系統通過使用
在 64 位系統中的使用:
在 64 位操作系統中,GS
寄存器通常用來指向線程的局部存儲區。操作系統和編譯器會將線程的局部數據結構映射到這個區域,允許每個線程訪問自己的線程特定信息。
看起來 GetCurrentThreadId 只需要執行幾條指令就能返回線程 ID
通過對線程本地存儲(TLS)的分析,得出了一個結論:獲取線程的唯一標識符(線程ID)其實并不復雜。為了獲取線程的 ID,可以通過 GS
段寄存器指向當前線程的線程局部存儲(TLS)。具體做法是,通過 GS
地址獲取線程本地存儲的位置,然后再通過這個位置獲得線程的 ID。
這就意味著,只需要通過兩條指令:第一條指令通過 GS
獲取當前線程的本地存儲地址,第二條指令從這個地址讀取出線程ID。實際上,線程本地存儲的地址本身就代表了線程的唯一性,因為每個線程的 TLS 地址都是獨一無二的。因此,線程 ID 就可以從這個地址派生出來。
從本質上講,獲取線程 ID 的過程非常簡單,只需要一個簡單的內存訪問操作就可以完成。
線程本地存儲(TLS)
線程本地存儲(TLS)是指在現代操作系統中,每個線程在啟動時會為自己分配一塊獨立的內存區域。這塊內存是專門為線程保留的,確保每個線程都有自己的私有空間,不會與其他線程的內存數據發生沖突。操作系統會為每個線程設置特定的內存段地址,這個內存段是與線程相關聯的,操作系統通過該段地址來管理和訪問線程本地存儲的數據。
具體來說,當一個線程啟動時,無論是主線程還是通過創建線程的方式生成的子線程,操作系統都會分配一塊內存,這塊內存是唯一且專屬于該線程的。操作系統在內存表中設置了一個特定的內存段地址,通過這個地址可以訪問該線程的線程本地存儲。在 x86 架構中,這通常是通過 GS
段寄存器來實現的,這個寄存器指向了線程的 TLS。
這種機制的核心是通過內存段來進行定位,雖然這源于早期計算機系統使用的分段尋址,但在現代操作系統中依然有效。每個線程都有自己的 GS
段寄存器值,指向線程本地存儲的位置,這樣每個線程就能訪問自己的私有內存區域。
分段尋址
在計算機系統中,內存尋址可以基于不同的段寄存器進行,而不僅僅是使用默認的虛擬內存段。GS
段寄存器就是一種特殊的段寄存器,它允許線程通過該寄存器來訪問與該線程相關聯的線程本地存儲(TLS)。線程本地存儲是每個線程獨有的內存區域,操作系統通過 GS
寄存器指向線程本地存儲的位置,使得每個線程能夠訪問屬于自己的數據。
每個線程在操作系統中切換時,操作系統會保存和恢復寄存器的狀態,包括段寄存器。當一個線程被搶占并且另一個線程開始執行時,操作系統會將當前線程的寄存器狀態保存,然后將新的線程的寄存器狀態加載進來。GS
寄存器就是其中一個關鍵的寄存器,它指向了與當前線程相關的內存區域,從而確保線程能夠訪問到屬于它自己的數據。
每個線程都有獨立的 GS
寄存器值,指向不同的內存位置,這意味著不同的線程訪問的內存區域是互不干擾的。通過這種方式,操作系統能夠確保每個線程都能夠安全地使用自己的內存空間,而不會影響到其他線程。
這種內存管理機制實際上源自早期的分段尋址技術,那個時候所有的內存訪問都是基于段的。隨著計算機架構的演變,分段尋址逐漸被現代的分頁機制所取代,但 GS
寄存器仍然在操作系統中用于處理線程本地存儲。在現代操作系統中,GS
寄存器作為一種優化手段,能夠避免頻繁的函數調用來訪問線程本地存儲,從而提高性能。
總的來說,GS
寄存器的使用讓每個線程可以通過簡單的內存地址偏移來訪問屬于自己的數據,而不需要復雜的函數調用。這種方法在多線程程序中非常有效,尤其是在操作系統層面進行線程管理時,能夠保證每個線程的數據隔離和獨立性。
有沒有什么內建指令可以讓我們復制 GetCurrentThreadId 的行為?
在討論如何獲取線程ID時,嘗試尋找一種直接的方法來避免每次調用函數的開銷。最初的想法是通過某種內建的指令(intrinsic)來直接獲取線程ID,而不是通過常規的函數調用。在分析過程中,考慮了是否可以利用特定的內建指令來實現這一功能。
嘗試通過不同的方式,包括使用 __rdtscp
或者其他類似的內建指令,來避免過多的函數調用。在查找過程中,發現了 move
和 access
等指令,但這些方法并不完全適用,或者沒有提供預期的線程相關功能。更進一步地,使用的操作系統和編譯器工具鏈中,某些功能(如fs
和 gs
段寄存器)僅在內核模式下可用,這使得在用戶模式下獲取線程本地存儲信息變得復雜。
另一個問題是,雖然可以使用一些內建的匯編操作來訪問線程相關的信息,但這些操作往往依賴于特定的系統或硬件配置,并且在不同的操作系統或處理器架構上可能表現不同。尤其是,在 Windows 系統中,gs
段寄存器的訪問通常被限制在內核模式,因此無法直接在用戶空間使用。
總的來說,盡管理論上可以通過某些低級指令來訪問線程信息,但由于操作系統的限制和架構差異,實際操作中會面臨一定的挑戰。對于線程ID的獲取,可能需要依賴更標準的操作系統接口,或者接受一定的性能開銷來獲取線程本地存儲信息。
有,_readgsqword
在討論如何獲取線程信息時,提到了一個關鍵概念,就是 gs
段寄存器,它被用來訪問線程本地存儲。這種方法正是所需要的,因為 gs
段寄存器是用于存儲特定線程的信息,從而實現線程間的隔離。這個思路啟發了進一步的操作,可能通過讀取 gs
段寄存器的某個特定數據項(比如 _readgsqword
)來獲取線程本地存儲中的信息,這恰好符合需求。
進一步的步驟是嘗試使用 _readgsqword
來獲取線程相關的數據,但由于這是一個不熟悉的領域,因此會采取實驗的方式來驗證其可行性。計劃通過查看該操作在執行時具體的行為,確保其能正確地提供所需的信息。此過程中,考慮了先直接查看 _readgsqword
的內容,確保能理解它到底在做什么,從而避免不必要的錯誤。
最終的目標是找到一種方法,能夠避免過多的函數調用,直接從硬件或操作系統提供的低級別指令中獲取線程信息,這樣可以減少性能開銷,并且為進一步的優化提供支持。
復制 GetCurrentThreadId 的行為
決定進入調試,逐步執行代碼,查看具體執行情況。在進行調試時,主要目標是確認操作是否如預期進行。通過逐步執行,可以更清楚地了解每一步的執行過程,特別是在操作 gs
寄存器時,確保獲取線程本地存儲的信息時不會發生錯誤。這個過程是為了驗證是否能夠通過低級別的指令,避免過多的函數調用,直接獲得線程相關信息,從而提高效率并減少性能開銷。
調試器:跳轉后進去看看
為了避免進行系統調用并簡化流程,通過反匯編分析,決定直接讀取 gs
寄存器中的線程本地存儲(TLS)信息。首先,假設從 gs
寄存器加載的地址指向線程本地存儲的位置。接下來,讀取該地址上的數據,獲取線程的標識符。由于 TLS 是一個指向內存的指針,因此通過讀取相應的四字節(Dword)可以獲取線程本地存儲的地址。然后,假設該地址偏移量為 48h
,可以通過該偏移量來訪問線程本地存儲中的數據,從而獲得線程ID。
這個過程不依賴于額外的函數調用,而是通過直接操作寄存器和內存地址來獲取線程ID,減少了不必要的性能開銷。
DWORD ThreadID = GetCurrentThreadId();
// 通過函數調用獲取當前線程ID
00007FF71E5C4823 call qword ptr [__imp_GetCurrentThreadId (07FF71E5CA190h)] // 讀取gs寄存器偏移48h處的數據,這通常是線程局部存儲(TLS)的一個值,指向當前線程的相關數據00007FFDC99B8620 mov rax, qword ptr gs:[48h] // 返回到調用處00007FFDC99B8629 ret
// 將獲取的線程ID存儲到變量ThreadID中
00007FF71E5C4829 mov dword ptr [ThreadID], eax
(void)ThreadID;
uint8 *ThreadLocalStorage = (uint8 *)__readgsqword(0x30);
// 通過gs寄存器讀取線程局部存儲(TLS)地址的偏移位置0x30
00007FF7956B482C mov rax, qword ptr gs:[30h]
// 將讀取的TLS地址存儲到ThreadLocalStorage指針變量中
00007FF7956B4835 mov qword ptr [ThreadLocalStorage], rax
uint32 ThreadID_2 = *(uint32 *)(ThreadLocalStorage + 0x48);
// 將ThreadLocalStorage指針值加載到rax寄存器中
00007FF7956B4839 mov rax, qword ptr [ThreadLocalStorage]
// 從ThreadLocalStorage偏移0x48的位置讀取數據,獲取線程ID
00007FF7956B483D mov eax, dword ptr [rax + 48h]
// 將獲取到的線程ID存儲到ThreadID_2變量中
00007FF7956B4840 mov dword ptr [ThreadID_2], eax
在 RecordDebugEvent 里記錄線程 ID
在調試過程中,成功獲取到線程ID后,考慮將其集成到代碼中。首先,想通過查看當前調試事件來確認這個線程ID,然后通過相關代碼操作,可以把線程ID和線程索引關聯起來。雖然線程索引現在只在某些特定的上下文中有用,可能需要一些額外的步驟才能確保它在所有地方都適用。為了避免處理線程索引時的問題,可以暫時將其設為32,這樣可以避免后續的復雜性。
在完成這些修改之后,整個過程似乎運行得很順利,線程ID和線程索引的操作也沒有遇到任何明顯的障礙。接下來,計劃繼續查看調試的行為,確保一切按預期工作。
總體來說,這個過程主要是調試并集成線程ID的獲取和管理,使得代碼更加穩定并能更好地處理線程相關的任務。
在平臺層抽象 GetThreadId
目前的目標是確保線程ID的準確性,并實現一個高效的性能分析工具。通過讀取gs
寄存器中的數據來獲取線程本地存儲(TLS),并使用合適的方式返回線程ID,避免了操作系統的干擾,確保在優化構建中這些操作能夠內聯執行,不會拖慢程序的性能。
通過這種方式,可以通過簡化的線程ID獲取方法替代GetThreadId
,并確保在不同平臺上也能夠有效運行。這種方法將不會導致任何操作系統相關的開銷,從而提高性能。線程ID的準確獲取,為進一步的性能分析提供了更可靠的數據支持。
在構建更好的調試視圖時,希望能夠展示一個更具可視化的時間線視圖,類似于餅圖的形式,能夠清晰地展示不同代碼部分所消耗的時間。通過將性能數據以這種方式展現,可以更容易地識別出性能瓶頸,進而優化程序。例如,觀察到某個藍色的條形圖占據了大量時間,而綠色條形圖占比很小,顯然藍色部分是性能問題所在。
接下來,計劃進一步改進調試視圖,構建一個更具交互性和可操作性的性能分析工具,并為后續的優化工作打下基礎。預計會在接下來的時間內深入進行這些改進。
統一平臺層和游戲層的計時器計數
今天的重點是去掉現有的調試框架和信息結構。如果還記得之前的實現,我們需要傳遞這些調試框架和信息參數,唯一的原因是當前平臺代碼無法正常使用調試計數器。但是現在希望找到一種方法,將這些調試計數器與平臺代碼整合到一起,使得可以在程序的兩側同時使用這些計數器。雖然這看起來有點復雜,但相信是可以實現的。
目前有三個編譯單元,理論上應該可以通過設置三個數組來實現,并且這三個數組應該能夠正確地寫入全局的調試計數器。關鍵在于如何統一管理這些計數器,使得在不同的平臺代碼和常規調試代碼中都能有效使用它們。雖然這有一定的挑戰,但在技術上應該是可行的。
將調試系統暴露給平臺層
目前面臨的主要問題是如何將 game_debug.h
中的代碼進行重構,使得它能更好地暴露出關鍵部分,特別是允許記錄調試事件的功能。這些功能需要能夠在 game_platform.h
中訪問。目標是確保能夠將這些功能整合到平臺層,使得不同部分的代碼能夠統一工作。
計劃開始著手處理這一問題,將 RecordDebugEvent
和計時塊的相關代碼提取到平臺層,以便它們能夠成為系統的一部分。這樣,平臺代碼和調試代碼的整合將更加順利。
在這個過程中,也發現了一些冗余的部分。比如,原先使用的 AtomicAddU64
等原子操作,現在已經不再需要了,所以可以將其移除。去除不必要的代碼后,留下的代碼就是我們真正需要的部分。特別是 RecordDebugEvent
的起始和結束部分,這些仍然是必須保留的,而一些其他的部分則可以去除,比如 HitCount
和其他不需要存儲的數據。
另外,某些原子操作和特定的函數調用(如 GetThreadID
)也是必須整合進來以支持更高效的功能。這些功能目前沒有自己的調用方法,因此需要為這些功能創建自己的調用機制。原子操作也需要集成進來,因為它們是實現關鍵功能所必需的。
總體來說,重構工作主要集中在清理代碼、整合平臺功能和移除不必要的部分。這一過程將有助于優化系統,使其能夠更高效地處理調試事件并提供精確的調試信息。
我們如何找到調試結構體的位置?
當前面臨的緊迫問題是如何找到正確的地址,以便為調試系統中的相關部分進行寫入。特別是對于一些如“調試事件記錄””這樣的數據,如何知道這些數據的確切位置就變得相對復雜。
在思考這個問題時,發現了一些關鍵點:系統中有兩個全局規則需要在調試過程中被訪問。這意味著,每次需要使用這些規則時,都必須知道它們的地址,也就是指向它們的指針在哪里。這兩個指針是獨立的,且都需要以某種方式被正確使用和尊重。
但目前還不確定最簡單的解決方案是什么,如何有效地管理和訪問這些指針仍然是一個挑戰。
我們可以把它們存儲在平臺層……
可以考慮將這些全局變量始終存儲在平臺層(platform layer)上。這樣,所有相關的全局值可以在平臺加載時進行初始化。這個做法應該不難實現,通過確保它們在平臺層加載時被正確初始化,可以有效地解決當前的問題。
……或者我們可以把調試數組放在 DLL 里,并通過 DLL 綁定來修正它們的地址
可以考慮利用綁定(deal binding)來處理全局變量的地址。雖然很久沒有做過這類操作,但通過綁定應該能夠順利地修補全局變量的地址。這樣一來,技術上就能夠讓整個系統正常運行,確保這一過程能夠順利完成。
通過 _declspec(dllexport) 進行 DLL 綁定
在處理數據導入時,可能需要通過導入地址表(Import Address Table)來管理數據的訪問。根據之前的討論,如果在構建函數時不使用導入地址表來訪問數據對象,就不會有間接訪問的問題。因此,數據可以繼續在原始位置(deal)中聲明和操作,而平臺則從中獲取數據。
雖然這種方法看似可行,但仍然存在一定的復雜性和不確定性。是否按照這種方式操作,確實有些棘手,需要進一步考慮和分析。
將所有調試結構體合并到一個單獨的結構體(debug_table)中
為了簡化當前的調試代碼,計劃將其結構進行合并。首先,考慮將調試相關的數據和數組整合到一個結構體中,這樣就不需要在多個地方分別聲明和管理它們。具體來說,可以創建一個結構體,該結構體包含調試記錄數組、事件數組的索引等內容,這樣就能把相關數據集中在一個地方管理。
為了進一步簡化,可以定義一個最大調試事件數量的常量,并通過這些常量來管理數組的大小和事件的數量。此外,結構體將包含所有必需的調試信息,這樣就不需要單獨聲明多個數組或指針,所有操作都能通過這個調試表來完成。
通過這種方式,只需要通過全局的調試表來訪問這些信息,而不需要在多個地方進行重復聲明。平臺層也可以成為獨立的翻譯單元(translation unit),只需通過該調試表進行訪問。
這種方法的好處是簡化了代碼結構,減少了冗余的聲明,使得調試代碼更加清晰、集中,所有操作都通過統一的接口進行管理。這種方式的最終目標是讓調試系統更加高效和易于維護。
用 TRANSLATION_UNIT_INDEX 預處理符號替代 RecordArrayIndexConstant
可以將這一結構做得更加正式一些,比如通過使用“翻譯單元索引”的方式來管理不同的翻譯單元。可以定義一個類似的結構或常量,來表示系統中有三個翻譯單元。這樣,我們就可以更加明確地知道有多少翻譯單元,并據此進行后續的操作。
這種做法看起來是合理的,能夠清晰地管理調試信息的結構,確保每個翻譯單元都能正確地處理相關的調試數據。通過這種方式,可以更有效地控制整個系統的調試流程,并減少可能的混亂和重復。
在調試相關的例程中只訪問 debug_table
在這一過程中,計劃將代碼進行一些簡化和清理。首先,原本存儲調試信息的部分已經不再需要,關鍵的部分是將全球調試表(GlobalDebugTable)提取出來并整合在一起,不再使用多個數組,而是直接使用這個統一的調試表。
-
移除不必要的變量:
許多不再需要的變量和數組已經被移除,減少了代碼的復雜度。例如,原本存儲調試記錄的數組、當前事件數組的索引等,都不再需要單獨聲明或處理。這些部分直接被納入了統一的調試表內。 -
簡化調試表的結構:
將調試表簡化為一個結構體,結構體中包含了必要的數組,例如DebugRecordArray
、DebugEventArrayIndex_DebugEventIndex
等。同時,清理掉了重復或不再需要的代碼,只保留核心部分,確保能夠統一訪問和操作這些調試數據。 -
全局調試表的訪問:
將原本分散的訪問方式統一為訪問全局調試表,確保各個翻譯單元都能夠通過這個全局表來獲取調試信息。通過這種方式,所有的調試相關數據可以集中管理,減少了代碼中冗余的部分。 -
去除冗余聲明:
不再需要多個地方聲明調試記錄、事件數組等數據,而是通過全局調試表來統一管理這些數據。具體實現中,所有調試記錄的計數器、索引等都通過這個表進行訪問和更新。 -
清理調試記錄計數:
將計數器整合到全局調試表中,原本在多個地方維護的計數器被合并為一個。通過這個方法,能夠簡化調試記錄的管理,減少不必要的復雜性。 -
命名規范:
對一些變量和數組的命名進行了清理和調整,使得命名更符合當前的結構和需求。例如,將DebugRecordArrayIndex
改為TranslationUnit
,讓命名更加清晰。 -
移除不必要的內部函數:
一些本來負責處理調試記錄的內部函數和操作也被移除,改為直接通過全局調試表來訪問和操作調試數據。這減少了代碼中的重復部分,并確保所有相關操作都集中在一個地方進行。
通過這些調整,整體代碼變得更加簡潔和高效,減少了重復的代碼,也提高了調試信息管理的集中性和可維護性。這些優化讓調試表成為了唯一的核心,簡化了程序的架構,同時保留了原有的調試功能。
測試。出現了一個 bug
目前代碼出現了一個問題,導致結果不是預期的那樣。通過快速檢查和回顧代碼,可能是翻譯單元(translation unit)以及不同的組織方式上出了問題。為了調試這個問題,進行了以下幾步檢查:
-
調試記錄的檢查: 首先,檢查了調試記錄部分,特別是計數器數組的設置,確保它正確地與翻譯單元(translation unit)相關聯。看起來這部分設置是對的,主要是在處理主記錄和優化記錄時使用了正確的翻譯單元。
-
平臺層檢查: 然后,檢查了平臺層的相關代碼,確認翻譯單元的設置是否正常。翻譯單元應該是按零、一個、兩個的順序設置的,而這個設置是符合預期的,看起來也沒有問題。
-
事件與記錄數組: 確認了事件數組和記錄數組的設置,看起來這些數組的定義和初始化都符合要求。沒有發現異常。
-
原子交換與事件索引: 在處理事件的過程中,檢查了原子交換(atomic exchange)的操作,特別是與當前事件索引相關的部分。確認了這是正確設置的,符合預期。
-
移除不再需要的部分: 代碼中不再需要的部分已經被移除,這部分也被認為是已不再相關,保證了代碼的簡潔性。
雖然大部分檢查看起來正常,但仍然沒有解決問題,可能是由于翻譯單元之間的交互或者索引管理上的細節問題。接下來的步驟可能需要更加深入地檢查具體的代碼實現,特別是在翻譯單元和事件處理的部分。
移除 GlobalDebugEventArray
問題的根源在于代碼中仍然存在一個名為“GlobalDebugEventArray”的部分,這是不應該出現的。這個數組在之前的代碼設計中已經不再需要了,然而它仍然存在,并且成為了導致錯誤的原因。
具體問題是,代碼中仍然在使用這個“GlobalDebugEventArray”,但它本應被移除或者替換為其他適當的結構。因此,應該去掉這個數組,確保代碼邏輯與當前的需求一致。這個問題的關鍵就在于這個不再需要的數組的存在,它需要被徹底清除或正確替換。
提高匯編水平的有效方法是什么?看書/教程?在 VS 里閱讀代碼反匯編?Mike Acton 只需看幾秒鐘代碼就能估算出執行周期和大致的匯編指令,我希望有一天也能達到這個水平
想要提高匯編語言的水平,可以采取幾種有效的方法,并且最好同時進行。
首先,閱讀匯編代碼是非常重要的。通過查看編譯器生成的匯編代碼,能夠逐漸理解它的運作方式。就像學習一門新語言一樣,你可以通過“沉浸式學習”,就像聽一部沒有字幕的法語電影一樣,逐漸理解其中的語言和模式。例如,在編寫代碼時,可以右鍵點擊查看匯編代碼,這樣可以幫助你更好地理解編譯器如何將高級語言轉換為匯編。
其次,深入學習一些基礎資料也很有幫助。比如閱讀《Intel架構手冊》,它詳細解釋了指令的格式和如何工作。即使只是偶爾讀一讀相關的內容,也能幫助加深理解和記憶,逐步建立起對匯編語言的感知。
最后,實踐是最有效的學習方式。開始編寫一些更接近匯編的代碼,例如內嵌匯編(intrinsic),這樣你就可以更直接地了解編寫的代碼如何與匯編語言對應。通過分析這些代碼,你會發現它們和匯編之間的差距會變得越來越小,也能夠更容易地理解編譯器生成的匯編代碼。
總的來說,閱讀、學習手冊和實踐編寫匯編代碼是提升匯編技能的三大步驟,通過不斷的實踐和積累,逐漸縮小你寫的代碼和最終匯編代碼之間的差距。
在一個應用程序中切換多個 API(如軟件/硬件渲染器)應該怎么處理?它的結構會類似于平臺層嗎?
在處理單個應用程序中切換多個API時,方法會根據具體的使用場景有所不同。無論是軟件還是硬件渲染,切換方式差異很大,因此首先需要明確的是:是為了接口與兩個相似功能的API進行切換,還是為了支持完全不同的渲染平臺。
1. API切換的基礎問題:
-
如果只是切換兩個功能相似的API(比如OpenGL與Direct3D),它們的本質非常接近,很多情況下,只是語法上的不同,底層驅動也往往相似。這種情況下,切換API的工作會相對簡單,因為大部分控制流程的邏輯可以放在平臺無關的部分,只需要在平臺相關的部分實現OpenGL或Direct3D特定的調用即可。
-
但如果是從一種API切換到另一種完全不同的API,例如從OpenGL與視頻擴展切換到iPad上的Metal,它們可能會有完全不同的控制流,甚至可能在不同的硬件平臺上有著不同的實現和架構。這樣就需要在架構層面進行更多的抽象和分層。
2. 控制流的分層:
-
當面對類似OpenGL與Direct3D的切換時,可以將大部分控制流邏輯放在共享的、平臺無關的部分,只在平臺特定的代碼中調用對應的API。這種做法能減少重復代碼并提高效率。
-
但在遇到差異很大的渲染API時(例如OpenGL與Metal),可能需要將控制流上移到更高層,這樣每個API的實現就可以單獨處理自己的渲染邏輯。這種方法雖然會增加開發的靈活性和定制性,但也會導致代碼復用的減少。
3. 平衡代碼復用與定制:
-
將控制流上移到過高層次會導致平臺相關代碼的復用減少,這樣雖然每個平臺可以有高效的實現,但如果平臺之間的API非常相似,這種做法會造成大量的重復工作。
-
另一方面,如果控制流過低,導致不同平臺的實現都在共享的控制流中進行適配,這樣在遇到差異較大的平臺時,可能會帶來更多的困難,導致實現變得復雜且低效。
4. 多層架構:
-
在某些情況下,可以采用多層架構來解決這個問題。比如,平臺無關層和平臺特定層之間,可能還會有一個共享的中間層,這個中間層專門處理一些平臺之間共有的邏輯。這樣可以保證不同平臺之間有部分共享的代碼,同時又能應對各自特有的需求。
-
通過這種方式,可以有效減少不必要的代碼重復,同時又能靈活地應對不同平臺間的差異。
5. API設計的重要性:
- API設計是一項非常復雜的任務,很多開發者在設計API時缺乏對架構和靈活性的考慮,導致很多API設計不夠優化,甚至存在很多問題。要設計高效、可維護的API,需要深刻理解如何在不同平臺和需求間做出正確的抽象和分層。
總的來說,在設計API切換時,關鍵在于選擇合適的層次來分隔平臺無關部分和平臺特定部分,平衡代碼復用與定制的需求,避免過于復雜的架構設計。這是一項重要的技能,盡管很少被充分重視,但它對架構設計的質量至關重要。
為什么編譯器不暴露 API 讓我們獲取代碼的信息(比如訪問 AST 等)?因為當我們寫一個自頂向下的解析器時,我們本質上是在寫編譯器已經擁有的東西
關于為什么API的設計和代碼應該包含更多信息,以及如何通過這些設計幫助我們的代碼,有一部分原因在于SEPA標準委員會的設計。其實,許多語言設計者在制定標準時犯了很多錯誤,這些錯誤導致了一些重要的功能沒有被一開始就包含進去。比如,編譯器提供的信息本應該在語言的設計之初就加以實現,但因為一些原因,這些功能一直沒有實現。
編譯器廠商并沒有太大動力去提供這類信息,因為他們知道大多數開發者需要面向多個平臺進行開發。如果他們提供了這樣的信息,實際上對開發者沒有太大幫助,因為他們并不打算針對某個特定平臺去開發。此外,編譯器廠商還需要設計并維護這些功能,而這并不是他們愿意去做的事情。
不過,歷史上確實有一些成功的例子。比如,IBM曾經開發過一個叫Montana的編譯器,它在設計上考慮到了許多開發者需求,提供了很多智能的功能。Montana編譯器采用了增量編譯的方式,可以避免需要聲明頭文件,簡化了開發流程。此外,它也有插件式的API,可以讓開發者通過編寫自己的插件來修改編譯過程,從而實現定制化的編譯。
但是,盡管Montana編譯器本身非常優秀,它卻被困在了一個名為Visual Age的產品中,遺憾的是,這個產品沒有獲得足夠的市場認同,最終也沒有得到廣泛應用。雖然它的設計理念和功能非常先進,但由于被鎖定在一個無法普及的產品中,許多開發者并沒有機會使用到它。
因此,雖然類似Montana編譯器的設計已經被嘗試過,并且實現得非常成功,但由于種種原因,這些優秀的設計并沒有被廣泛推廣和應用。今天,雖然我們依然面臨類似的挑戰,但遺憾的是,過去的成功經驗并沒有成為編譯器設計的主流。
你還在用軟件渲染器,還是已經轉向 DirectX 或 OpenGL?
目前仍然在使用軟件供應商的情況,并且在渲染過程中遇到了困難。這個問題通常涉及多個方面,尤其是當軟件需要適配不同平臺時,可能會面臨性能和兼容性的問題。在渲染的過程中,依賴特定供應商的工具和框架,可能導致效率低下或者在某些平臺上的不兼容性。隨著技術的發展,很多時候開發者需要尋找更優化的解決方案,避免長期依賴某一特定供應商的產品,尤其是在遇到技術瓶頸時。
這類問題可能會影響到開發效率和最終產品的性能,因此需要考慮是否需要換用不同的供應商,或者考慮其他更為高效、靈活的技術方案。同時,也要考慮如何在多個平臺上確保渲染的一致性和性能表現,可能需要對現有的渲染流程進行調整或優化。
什么時候一個人算是“好程序員”?你覺得自己是什么時候成為一個好程序員的?
一個人成為優秀程序員的時刻是一個漸進的過程。一個人從七歲開始編程,到十六歲時才真正意識到自己在編程上有所進步。當時,十六歲被認為是一個轉折點。然而,這個進步并不是一蹴而就的。十九歲時,接觸了C++編程語言,并受到當時流行的技術影響,誤認為它很優秀,因此變得相對較差。這個階段大約持續了四到五年。
直到后來,在一些良師益友的幫助下,逐漸意識到自己過去的技術選擇其實并不理想,慢慢回歸到正確的編程路徑上。這些朋友通過身教而非言教,幫助重新認識到技術的本質,最終明白了哪些才是真正值得追求的技術。這個過程中的支持和不斷的自我反思,是走向編程之路的重要環節。
最終,經過一段時間的調整,重新找回了編程的方向,并從那時起,成為了一名比較成熟的程序員。這個過程證明了成長和反思對于成為優秀程序員的重要性,尤其是要有一個能幫助自己進步的環境和一些啟發性的引導。
你是如何應對程序員的倦怠/抑郁的?那些你完全不想做任何事、不高效、沒有動力、甚至寧愿做任何別的事情也不愿意寫代碼的日子
應對程序員的職業倦怠和抑郁,特別是那些日子里感到無法做任何事、情緒低落甚至無法完成最基本的任務時,最重要的是不要過于擔心這種情況,因為每個程序員都會經歷類似的困境。解決這種困境的一個有效方法是每天至少寫一個小時的代碼,不論這段代碼的質量如何。
即使是在遇到瓶頸,感覺無法解決問題時,也要堅持編寫代碼。寫的代碼不一定要完美,也不必擔心之后會重寫。關鍵是通過寫代碼來保持進展。因為通常一旦開始寫代碼,逐漸克服困難后,就會發現自己能繼續前進,并且逐漸擺脫低谷。心理上,最能阻止繼續前進的并非缺乏知識,而是感覺自己卡住了,認為無法繼續。
當面對困難時,重要的是通過嘗試和實驗,逐步寫入代碼并運行編譯器。通過設定小目標,逐步取得進展,能夠幫助走出低迷。保持編程的連續性非常關鍵,否則如果讓自己完全停滯不前,可能會因為長時間沒有產出而更加陷入困境。因此,保持持續的代碼輸入,不管結果如何,最終會幫助恢復動力,走出困境。
你真的更愿意省下兩個無條件跳轉(以免 CPU 管道被清空)而不是選擇編譯器和平臺無關性嗎?(指 GetThreadId 的實現方式)
首先,討論的是平臺獨立性的問題。實際上,并不存在完全的“平臺獨立性”。因為在不同平臺上,獲取線程 ID 的方式是不同的。例如,在 Windows 和 macOS 上,獲取線程 ID 的方法肯定不同。因此,需要為不同的平臺調用不同的函數。換句話說,平臺獨立性在這里并不適用。
真正的目標是編譯器獨立性。每個平臺都有不同的編譯器,我們通常在 Windows 平臺上使用某個特定的編譯器,而在 macOS 上使用另一個編譯器。如果我們切換平臺,只需要切換對應的平臺編譯器即可,并不需要依賴多個編譯器來實現代碼的編譯。
此外,提到為什么要在每個計數器中加入跳轉指令,這似乎是沒有必要的,因為這樣做并不會帶來任何實際的好處。這樣做反而可能是一個糟糕的選擇,增加了不必要的復雜性。
你為什么做游戲引擎而不是直接做游戲?
首先,制作游戲引擎和制作游戲在某種程度上是緊密相連的,但也有明顯的不同。決定從頭開始制作一個引擎是為了展示如何制作游戲引擎的每一個部分,讓所有人都能看到過程和細節。雖然制作游戲和引擎看似是兩個不同的目標,但實際上,它們的實現是高度關聯的。制作引擎本身是為了展示如何實現引擎的各個功能,而游戲的開發則是為了讓這些引擎的功能得以體現和應用。
選擇先制作引擎的原因主要有兩個方面。首先,作為引擎程序員,目標是展示如何編寫一個引擎,而不僅僅是制作一個游戲。游戲的制作本身屬于另外一個領域,而引擎編程是一個獨立的專業,專注于引擎的功能和實現。其次,制作游戲引擎時,很多復雜的部分需要通過游戲來激勵和展示。例如,當我們在編寫碰撞檢測或對象破碎的代碼時,必須有一個游戲場景作為背景,才會有動力去做這些事情。如果沒有游戲,很多引擎的功能就失去了意義。
因此,選擇同時做引擎和游戲,是因為這樣能夠更好地理解為什么要編寫某些功能。通過游戲的場景和需求,可以清楚地知道為什么要實現某些特性,哪些功能是必須的,哪些是根據具體游戲需求設計的。比如,在設計一個能夠支持巨大世界的引擎時,需要考慮如何存儲世界的部分信息。如果沒有具體的游戲需求,就不容易理解為什么要實現這樣的功能。
總之,做游戲引擎和游戲的結合,可以幫助更好地理解為什么做這些代碼,并讓學習者更清楚地看到代碼背后的動機和原因。這樣的方法不僅能幫助學習者理解如何實現引擎功能,還能激勵他們在學習過程中理解每一個步驟的必要性。
你覺得實現一個正則表達式匹配器對元編程來說有意義嗎?
關于“正則表達式”是否在編程中重要的問題,回答是不太看重正則表達式。其實,正則表達式這個概念一直都不是很受歡迎,個人也不喜歡正則表達式的相關內容。原因是,正則表達式往往顯得過于簡單化,它在很多情況下并沒有太大的實際用途。對于一些編程任務來說,使用正則表達式顯得有些局限,不夠靈活,且沒有足夠的深度去解決復雜的編程問題。因此,正則表達式在編程中并不是很有價值,尤其在需要復雜計算和多樣化處理的場景下。
你似乎對現代軟件的質量很不滿。你覺得以前的程序員更厲害嗎?你自己寫的軟件總是完美無缺的嗎?我是認真問的,想更清楚地理解你的觀點,謝謝γ
在編程的歷史上,過去的程序員確實普遍更優秀。過去編程是非常困難的,只有那些具備一定技能和知識的人才能真正從事編程工作,因此通過自然選擇,優秀的程序員才能脫穎而出。相比之下,現在編程變得容易了很多,幾乎任何人都能輕松地編寫代碼。如今,編程不再是一個專門的領域,任何人都可以拿起一段腳本開始編程。結果是,雖然優秀的程序員數量并沒有顯著變化,但大量不合格的程序員涌現出來,造成了很多軟件的質量問題。
很多軟件今天的質量問題就是因為程序員的水平差異。以Facebook為例,Facebook的iPhone應用程序質量非常差,很多人也注意到了這一點。盡管如此,Facebook卻沒有表現出任何改進的意圖,反而在一些博客文章中自夸其成就。這種做法令人非常不解,因為應用程序的質量問題顯而易見,而他們卻選擇忽視,甚至試圖為它辯解。如果他們承認問題并表示道歉,或許會顯得更為誠實。
雖然自己對代碼質量有較高的要求,但也承認自己并不是最嚴格的標準。例如,與一些極為注重性能的程序員相比,自己容忍的性能問題要寬松得多。自己認為可以接受的性能問題,可能會被一些優秀的程序員視為不可接受的。
總體來說,隨著行業的變化,很多人對軟件的可靠性和性能的接受度已經大幅下降,導致了許多低質量的產品流入市場。自己始終沒有超越那些自己尊敬的程序員的高標準,甚至覺得自己在很多方面比不上他們。但是,令人擔憂的是,很多人已經習慣于接受低質量的產品,甚至忽視了這種現象帶來的后果。
因此,結論是,如果你的軟件質量和性能水平低于一個基本的標準,那么你就非常危險了。自己雖然不是最高標準,但如果連自己設定的標準都沒能達到,那就真的很糟糕。