游戲引擎學習第125天

倉庫:https://gitee.com/mrxiao_com/2d_game_3

回顧并為今天的內容做準備。

昨天,當我們離開時,工作隊列已經完成了基本的功能。這個隊列雖然簡單,但它能夠執行任務,并且我們已經為各種操作編寫了測試。字符串也能夠正常推送到隊列中,整體運作看起來很順利,雖然沒有進行深入的測試,但基本上已經能夠正常工作。

不過,唯一沒有完成的部分是主線程的處理。當前主線程的工作方式是,當它執行某些任務并將工作推送到隊列時,會進入一個自旋鎖,等待其他線程完成。這種做法存在兩個問題:

  1. 浪費電池:主線程會一直等待其他線程完成任務,這樣就浪費了電池,因為線程并沒有做任何有意義的工作,僅僅是在等待。

  2. 浪費性能:如果主線程只是一直檢查某個變量,重復地等待其他線程完成任務,這無疑是浪費了寶貴的處理器時間。這樣做并不高效,因為可以讓主線程去做其他有意義的工作,而不是空轉。

因此,我們建議,主線程不應該無休止地等待。相反,主線程在將任務分配給其他線程后,可以自己也做一些工作,而不是等待其他線程完成。如果主線程需要等待,那么它可以在等待的過程中處理其他任務,而不是僅僅浪費時間和性能在等待上。

這樣做不僅能避免浪費電池,還能提高性能,因為主線程將不再在等待時空閑,而是可以利用這些空閑時間去執行其他任務。這種方法會更加高效、合理。
在這里插入圖片描述

win32_game.cpp: 引入 DoWorkerWork 函數。

在這里插入圖片描述

我們提出了一個新的方案,目的是優化線程的工作方式。具體做法是,在線程的處理過程中,我們將引入一個內聯函數,暫且叫它“DoWorkerWork",這個函數的作用是返回任務池的處理結果。

這個函數會執行一些原本在主線程中進行的工作,也就是我們之前討論過的任務處理內容。通過這種方式,線程會在檢查工作狀態時,直接執行任務,并返回是否成功完成了工作。具體來說,線程在執行時,會標記是否完成了某些工作,然后返回這個狀態。

一旦完成了這個檢查,我們可以設置一個標志,比如“DidSomeWork”,初始化為“false”,然后在工作執行完成后,更新為“true”。這樣,每次通過檢查時,我們都會確認是否完成了工作,并返回這個狀態。

接下來,我們可以在判斷任務是否完成后,如果線程沒有做任何工作,就讓它進入休眠狀態。這樣,線程就不再處于空轉狀態,而是等待下一個任務的到來。

為了實現這個優化,我們需要傳遞一些參數給函數,比如線程的邏輯索引。這樣就可以確保線程能夠正確地處理任務并根據需要進入休眠。最終,這個過程能有效地減少空閑時間,提高效率,讓每個線程在完成工作時能夠及時休息,避免不必要的資源浪費。

讓我們的常規線程開始工作。

通過這種方式,現在我們可以讓主線程在執行任務時,持續地進行有效工作。具體來說,我們可以使用八個線程來并行處理任務,其中七個線程用于實際的工作,而第八個線程則作為主線程。

主線程的工作機制是,雖然它負責進行任務排隊(即將任務分配給其他線程),但它并不會在排隊后完全閑置。主線程會在整個過程中保持活躍,執行一些實際的工作。它會通過自旋鎖的方式稍微等待其他線程完成任務,但這種自旋鎖只會出現在非常末尾的地方。這樣,我們就可以避免讓任何一個線程處于完全閑置的狀態。

我們希望通過這種設計,即使是在任務排隊完成后,主線程也能持續地執行有意義的工作,只有在所有任務都完成后,主線程才會停止工作并結束。這種方式確保了主線程在沒有任務可做時才會結束,而在有工作的時候它會全力運作,保持高效。這樣就能最大化地利用每個核心的處理能力,避免任何一個核心處于空閑狀態。
在這里插入圖片描述

運行并查看線程在做什么。

如果現在運行這個方案,我們應該能看到與之前相同的行為。主線程仍然會等待,任務不會立即結束,但我們能夠看到主線程(線程7)實際上也在執行一些工作。這正是我們想要的效果。

通過這種方式,我們避免了使用信號或其他機制來讓主線程休眠,而是讓主線程在沒有任務時持續工作。這樣,主線程就不會完全閑置,始終能夠做一些實際的工作,直到所有任務都完成。

目前來看,這種方式是可行的,并且能夠保持高效。我們可以等到最終發布時再回顧是否有改進的空間,如果出現任何不合適或性能問題,再做調整。但現在,這種做法應該是最優的。
在這里插入圖片描述

在這里插入圖片描述

遵循面向壓縮的編程方法。

現在我們要做的工作是將現有的代碼轉換為一個格式,使得渲染系統能夠實際使用這個多線程的功能。需要做的是將當前的多線程處理部分整理成一種可以被平臺無關的代碼所利用的形式。不過,目前我們還沒有完全確定要采用什么樣的方式。

通常情況下,我們會在開發過程中邊做邊設計,而不是事先有固定的設計思路。這樣做的目的是讓大家能夠看到我們是如何一步步解決問題的。我并沒有預設好要怎么做,而是打算從現有的代碼開始,嘗試一下,看看能如何調整和優化。

正如我們所提到的,我傾向于采用漸進式編程的方式:先把功能做出來,運行成功后再回頭思考如何優化、簡化,或者如何把一些可復用的部分提取出來。接下來,我們將直接進入 render group 部分,快速瀏覽一下,看看如何將渲染部分多線程化。

目前,渲染部分有一個問題需要解決,這個問題與多線程有關,我們會稍后討論。雖然它不會導致程序崩潰,但可能會導致一些不理想的結果。這個問題我們可以等到后續再處理。但現在,重點是關注如何讓 TiledRenderGroupToOutput 能夠輸出,并實現多線程化的處理。
在這里插入圖片描述

game_render_group.cpp: 研究如何在多個線程上執行 TiledRenderGroupToOutput。

接下來想要做的事情是,當調用渲染函數時,能夠將工作分配到多個線程中。具體來說,目標是將渲染中的每個瓦片(tile)分配給不同的線程處理。

目前,渲染函數的調用是在一個 for 循環內進行的。每個瓦片會按順序依次處理,問題在于如何讓這個 for 循環能夠并行化,使得多個線程能從循環中獲取任務并同時工作。

第一步是想要實現的是,不用完全改變現有結構的情況下,使得這些瓦片能在多個線程中獨立處理。可能的方案是通過某種方式,使得每個瓦片任務能被多個線程并行地拉取和處理。這個過程并不需要完全重構現有的代碼,而是可以通過調整工作隊列(work queue)的方式,利用現有的通用隊列進行任務分配。

工作隊列(work queue)已經是一個通用的工具,可以用來管理這些任務。因此,直接在隊列中推送這些任務,分配給多個線程來處理,是一種合理且簡單的解決方案。這種方式可以使得任務分配和線程管理更為簡潔,并且不需要對原有的代碼做過多復雜的修改。

game_platform.h: 考慮引入 work_queue_entry。

現在決定將工作隊列的處理進行抽象,直接將其提取出來,做成一個可以在平臺相關的頭文件中使用的功能。這意味著,工作隊列的條目(work_queue_entry)將被獨立出來,讓不同的游戲模塊都能夠使用。

首先,工作隊列條目需要以一種平臺無關的方式進行處理,因此會在代碼中定位到一個合適的位置,可能是在關于調試循環計數器的一些內容之后,接著進行這部分的實現。目標是讓這部分代碼在平臺之間能夠通用,簡化代碼的管理和復用。

接著,考慮到工作條目的大小問題,最初的想法是使用固定大小的條目,但后來覺得可以使用可變大小的條目。經過考慮,使用可變大小的工作條目幾乎不會帶來額外的性能損失,因此決定繼續推進這一方案。

總之,接下來的步驟是將這部分內容進一步抽象化,確保工作隊列可以更加靈活地應用,并在不增加額外復雜度的情況下,支持可變大小的工作條目。這種方式能夠使得代碼更具通用性和可擴展性。

win32_game.cpp: 將 PushString 重寫為 AddWorkQueueEntry。

現在的目標是確保能夠安全地將任務推送到工作隊列中。當前的情況是我們只需要一個生產者線程來將任務放入工作隊列,而多個消費者線程會從隊列中取出并處理任務。所以目前并不需要考慮多個生產者同時向隊列添加任務的情況。

考慮到我們目前只需要單生產者-多消費者的模式,且暫時沒有考慮到多個生產者的需求,我們決定將添加任務到隊列的功能封裝成一個名為 AddWorkQueueEntry 的函數。這個函數的作用是將一個新的任務條目添加到工作隊列中,并通過信號量來同步操作。

信號量的管理會通過一個全局變量來完成,具體的工作隊列會被封裝在平臺層(platform layer)中。這樣,游戲邏輯層將不會直接接觸到工作隊列的實現,而是通過平臺層來與工作隊列交互。平臺層將負責將任務條目放入隊列,并確保正確的同步機制。

AddWorkQueueEntry 函數中,我們將不直接傳入任務條目的具體內容,而是假設任務條目已經在之前的步驟中被正確填充。該函數的主要作用是確保任務被成功加入到隊列中,并釋放信號量來通知消費者線程。具體來說,我們會在函數內部管理信號量的增加和計數,確保多個消費者線程能夠有效地從隊列中獲取任務。

此外,我們還會在工作隊列中維護一個 EntryCount 變量來跟蹤當前隊列中任務條目的數量。這個計數器將被聲明為 volatile 類型,確保在多線程環境中能夠正確地更新和訪問。

通過這種設計,我們不需要將工作隊列的管理細節暴露給游戲邏輯層,而是將所有的實現封裝在平臺層中。這樣可以確保代碼的清晰性和可維護性,同時也能避免將平臺相關的代碼混入游戲邏輯中。最終,這種方式能夠保證任務的正確同步和多線程的高效執行。
在這里插入圖片描述

注意 _mm_sfence 的必要性。

我們昨天討論了一些關于內存排序的問題,今天我們又仔細檢查了一下,特別是關于在x64(可能是指某種系統或架構)中的正確順序。我們發現,昨天說的內容可能已經不準確了。經過再次確認,看起來x64的內存排序規則似乎已經不像以前在x86那樣強了。之前,我們以為它的寫排序(write ordering)還是很強的,但現在情況似乎變了。

具體來說,我們注意到,在x64架構上,內存排序的嚴格程度好像被削弱了,不像之前那樣可以完全依賴硬件來保證順序。為了確認這一點,我們打算再做一些研究,并且打算向Intel那邊的一些人咨詢一下,獲取更準確的信息。不過,從目前觀察到的現象來看,在x64上要想確保內存操作的順序,尤其是寫操作的順序,可能確實需要顯式地加入一些措施。

比如,我們覺得現在有必要在代碼中加入存儲屏障(store barrier),以確保寫操作按預期順序完成。以前,可能不需要這么做,硬件自己就能處理好,但現在看起來情況變了,存儲屏障成了必須的。幸好我們在代碼里已經加了這個屏障,看來這個決定是對的,它確實是必要的。

不過,我們還是覺得需要再多了解一些細節,不能完全確定現在的判斷就是最終結論。所以接下來,我們會繼續研究一下,確保徹底弄清楚這件事。但就目前而言,我們傾向于認為,在x64上確實需要加入存儲屏障來保證順序。以前可能不是這樣,但現在情況已經不一樣了。總之,我們會繼續跟進這件事,確保萬無一失。

將 work_queue_entry 拉入測試代碼中。

我們正在討論一個編程相關的實現方案,主要是關于如何在代碼中加入一個讀取屏障(_ReadBarrier()),并且對工作隊列(DoWorkerWork)的處理進行優化和測試。以下是詳細的總結:

首先,我們考慮在現有的代碼結構中加入一個讀取屏障。這個屏障的作用是為了確保在多線程環境下,數據的讀取操作能夠按照預期順序進行,避免出現數據競爭或不一致的問題。我們計劃將這個屏障的具體實現放到測試代碼中,而不是直接修改上層的核心代碼。這樣做的目的是為了方便后續的調整和實驗,同時保持主要代碼的簡潔性。

接著,我們將注意力轉向了工作隊列的條目(work_queue_entry)。我們決定把這個部分拉到測試代碼中進行處理,而不是讓上層的代碼直接操作它。這樣,我們就可以更自由地對工作隊列的邏輯進行測試和調整。為了實現這一點,我們打算把工作隊列條目設置為一個全局變量,但這個全局變量會被放在線程塊(thread blocks)之下。這樣設計的好處是,上層代碼無法直接訪問到它,從而保證了一定的封裝性,同時我們也能在測試時更方便地操作。

在測試階段,我們的目標是模擬和管理工作隊列的行為。我們注意到,工作隊列中的每一個條目都需要包含一個信號量句柄(SemaphoreHandle),這個句柄在任務完成時需要被釋放。此外,每個條目還會有一些隊列相關的元素,比如指針(pointer)等,用于追蹤和管理隊列的狀態。為了更好地控制隊列的行為,我們還考慮加入一個最大計數(MaxEntryCount)的機制,用來限制隊列的規模,確保它不會無限制增長。這個最大計數可能會通過宏定義(macro)的方式實現,方便在代碼中調整和維護。

總的來說,我們的計劃是先在測試代碼中搭建一個獨立的環境,用來實驗讀取屏障和工作隊列的實現。通過將這些功能下放到測試層,我們既能保證核心代碼的穩定性,又能靈活地調整和優化這些新特性。接下來,我們會基于這個框架進行更多的調試和驗證,確保整個系統的可靠性和性能。
在這里插入圖片描述

將 DoWorkerWork 拆分成兩部分。

我們現在正在深入探討如何實現工作隊列中工作線程的具體操作邏輯,特別是關于“DoWorkerWork”(worker work)的設計和實現。以下是詳細的總結:

我們計劃將“DoWorkerWork”的實現分為兩個部分來處理。第一部分是“BeginWorkQueueWork”,第二部分是“EndWorkQueueWork”。這兩個部分共同構成了工作線程從隊列中獲取任務并完成任務的完整流程。雖然“BeginWorkQueueWork”這個名字聽起來有些奇怪,但它確實反映了我們想要實現的功能。

在“BeginWorkQueueWork”階段,我們的目標是從工作隊列中取出一個條目(work_queue_item)。具體來說,這個操作會返回一個條目索引(entry index),用來標識當前需要處理的工作項。我們考慮讓這個索引以32位整數(int32)的形式返回,因為這樣可以滿足大多數場景的需求。為了簡化設計,我們可能會去掉一些不必要的復雜邏輯,專注于核心功能。

而在“EndWorkQueueWork”階段,我們需要完成任務的后半部分處理。這部分的主要操作是執行一個線程安全的遞增(InterlockedIncrement),用來標記某個工作項已經完成。這個操作同樣基于隊列進行,確保多線程環境下的數據一致性。我們注意到,這兩個階段的操作實際上與之前代碼中某些部分的邏輯高度相似,因此我們可以復用一部分現有實現,只需稍作調整即可。

為了讓這兩個部分更好地協作,我們設計了兩者都需要接受隊列(queue)作為參數。“BeginWorkQueueWork”會返回當前需要處理的工作項信息,而“EndWorkQueueWork”則負責更新隊列狀態,確認任務完成。具體來說,在“BeginWorkQueueWork”中,我們會從隊列中取出一個條目,并將其索引返回給調用者。而在“EndWorkQueueWork”中,我們會通過遞增操作更新隊列的完成計數。

為了讓這個機制更靈活,我們考慮定義一個返回值結構,比如叫做“work_queue_item”(work queue item)或者“工作隊列結果”(work queue work result)。這個結構包含兩個字段:一個布爾值(bool),表示是否存在可處理的工作項(exists);另一個是工作項的索引(index),用來標識具體的工作條目。如果隊列中沒有工作項,我們會返回一個結果,其中“exists”設為false,索引可以是無效值;如果有工作項,則“exists”設為true,并將對應的條目索引填入。

這個設計的實現過程非常直觀。在“BeginWorkQueueWork”中,我們檢查隊列狀態,如果有可用的工作項,就將其索引提取出來,設置“exists”為true,然后返回結果;如果隊列為空,則直接返回“exists”為false的結果。在“EndWorkQueueWork”中,我們只需要對隊列的完成狀態進行更新,確保任務被正確標記為已完成。
在這里插入圖片描述

將 while(EntryCount != EntryCompletionCount) 放入 QueueWorkStillInProgress 函數中。

我們計劃從當前代碼中提取一部分功能,把注意力集中在與工作隊列相關的調用上。具體的想法是處理“EntryCount”和“QueueWorkStillInProgress”,并基于此新增一個功能調用。這個新調用的名字初步定為“所有工作是否完成”(is all work completed),它的作用是檢查工作隊列的狀態,判斷所有任務是否都已經處理完畢。

在實現“所有工作是否完成”這個功能時,我們的思路很簡單:通過比較隊列中的兩個關鍵值來判斷狀態。這兩個值分別是隊列中任務的總數和已完成任務的數量。如果兩者相等,就說明隊列中的所有工作都已完成。我們注意到,之前的設計中可能已經用到了類似的邏輯,但方向可能是反的——之前可能是判斷“是否存在未完成的工作”,而現在我們想要明確檢查“是否全部完成”。所以,我們決定調整邏輯的方向,把它定義為“QueueWorkStillInProgress”。

具體來說,這個新調用的邏輯是這樣的:如果隊列中的任務總數和完成計數不相等,就說明還有未完成的工作,也就是“隊列工作仍在進行”返回true;如果兩者相等,則返回false,表示所有工作都已完成。我們覺得這樣的設計更符合直覺,也更適合后續的使用場景。比如,我們可以用這個調用來判斷是否需要繼續處理隊列中的任務。如果“隊列工作仍在進行”返回true,我們就繼續工作;如果返回false,就說明可以停下來了。

為了實現這個功能,我們會在現有的隊列操作基礎上新增這個檢查邏輯。實現起來并不復雜,只需要從隊列中獲取當前的條目計數和完成計數,然后進行比較即可。我們還考慮到了代碼的可讀性和簡潔性,盡量避免引入過于復雜的條件判斷,直接用一個簡單的比較就能解決問題。
在這里插入圖片描述

重命名并完成這些函數的編寫。

我們現在正在完善工作隊列的實現細節,主要是圍繞如何更邏輯化和清晰地處理隊列中的工作項,以及如何優化相關的函數調用和數據結構設計。以下是詳細的總結:

我們首先考慮如何更好地組織工作隊列的操作流程。最初的設計是用“BeginWorkQueueWork”來獲取一個工作項(work_queue_item),并用“EndWorkQueueWork”來標記完成。但我們覺得這個命名和邏輯可以更直觀一些。于是,我們決定調整為“GetNextWorkQueueItem”(獲取下一個工作隊列項)這樣的方式,這樣更符合操作的實際含義。我們會從隊列中取出一個工作項,并檢查它是否有效(IsValid)。如果有效,就執行相應的任務;完成后,再調用一個函數來標記該工作項為已完成,比如“MarkQueueEntryCompleted”。

在具體實現上,“GetNextWorkQueueItem”會返回一個工作項,我們用一個布爾值(IsValid)來表示這個項是否存在。如果存在,我們就處理它,然后通過“MarkQueueEntryCompleted”更新隊列狀態。這個完成標記函數需要接收隊列和具體的項作為參數。雖然當前我們可能并不直接需要工作項本身的數據,但為了保持設計的靈活性和可讀性,我們還是決定讓它接受這個參數,方便未來可能的擴展。完成標記的核心操作是對隊列的計數進行線程安全的遞增(InterlockedIncrement),以反映任務的完成情況。

接下來,我們開始優化整個工作流程的代碼結構。我們希望讓“DoWorkerWork”(執行工作者任務)函數能夠基于隊列直接操作,而不需要過多的額外參數。之前的設計中,這個函數可能需要同時處理隊列和工作項索引,但現在我們覺得可以簡化邏輯,讓它直接依賴隊列本身。工作項的索引(entry index)可以從隊列中動態獲取,不需要顯式傳遞。這樣,代碼的抽象層次更高,也更符合平臺無關的實現目標。

在這個過程中,我們還調整了工作隊列條目(work_queue_entry)的命名和使用方式。一開始,我們在“item”和“entry”這兩個詞之間有些混淆,但最終決定統一使用“entry”來表示隊列中的工作條目。這樣在代碼中,比如“AddWorkQueueEntry”(添加工作隊列條目)這樣的調用會更一致。我們還特別設計了一個場景:假設需要打印字符串,我們會通過“AddWorkQueueEntry”將字符串任務加入隊列,然后由“DoWorkerWork”根據返回的條目索引(Entry.Index)來處理具體的打印操作。

為了支持這個流程,我們新增了一些輔助函數。比如“GetNextAvailableWorkQueueIndex”(獲取下一個可用工作隊列索引),這個函數會從隊列中返回當前可用的條目索引。因為我們假設這是一個單生產者(single producer)的場景,所以這個操作相對簡單,不需要復雜的同步機制。我們會先獲取索引,然后將對應的條目信息寫入隊列,確保寫入完成后才更新隊列狀態。這樣可以保證數據一致性。

在實現隊列本身時,我們注意到它的初始化非常簡單:只需要將計數清零,并設置好信號量句柄(SemaphoreHandle)。更進一步,我們發現信號量句柄其實可以直接從隊列對象中獲取,不需要單獨傳遞。這樣,代碼又精簡了一層,函數調用只需傳入隊列本身即可。比如在“AddWorkQueueEntry”中,我們會基于隊列直接操作,添加條目并更新索引。

最后,我們回顧了整個流程的邏輯:在“QueueWorkStillInProgress”(隊列工作仍在進行)的條件下,工作線程會持續從隊列中獲取任務并處理。我們確保“DoWorkerWork”能夠正確接收隊列參數,并基于此執行任務。整個設計的重點在于清晰的抽象和簡潔的實現:通過“GetNextWorkQueueEntry”獲取任務,“MarkQueueEntryCompleted”標記完成,再加上必要的隊列管理函數,構成了一個完整的工作隊列處理框架。

總的來說,我們現在的方案已經逐步成型。通過調整命名、優化函數簽名和簡化參數傳遞,我們讓代碼更具可讀性和可維護性。接下來,我們會繼續驗證這些功能的實現,確保它們在多線程環境下能夠穩定運行。一步步推進,我們的目標是打造一個高效且易用的工作隊列系統。
在這里插入圖片描述

編譯并運行,查看線程的執行情況。

在這里插入圖片描述

討論我們的選項。

我們現在已經完成了一個抽象化的工作隊列版本,這個版本可以直接集成到平臺的代碼中,特別是在游戲開發相關的場景中。以下是詳細的總結:

我們設計的這個抽象隊列已經可以很好地支持任務的添加和移除操作。具體來說,當任務通過某個圖形處理流程(graph)時,我們可以利用這個隊列來實現任務的入隊(enqueue)和出隊(dequeue)。這樣,游戲代碼就可以通過調用平臺的隊列功能來管理任務。我們注意到,通常情況下,我們并不傾向于頻繁地在游戲和平臺代碼之間來回調用函數(round trip functions),因為這可能會增加復雜性和開銷。所以在添加這類回調機制時,我們會盡量保持謹慎,只在必要時引入。

考慮到性能問題,我們面臨兩個主要的選擇:一是將隊列操作內聯(inline)到游戲代碼中,以減少函數調用的開銷;二是直接通過函數指針傳遞這些操作,讓游戲代碼動態調用。我們仔細權衡了兩者的優劣。如果追求極致的運行速度,內聯顯然是更好的選擇,因為它能消除函數調用的額外代價。但我們也觀察到,這些隊列操作本身的代碼量非常小,每次調用的邏輯也很簡單。而且,在實際應用中,任務的處理頻率并不高,可能每秒只有50到60次操作,遠沒有達到需要大量調用的程度。因此,通過函數指針調用的開銷其實是可以接受的。

在這種情況下,我們開始思考是否需要保留所有的函數接口。最初的設計包括“GetNextAvailableWorkQueueIndex”(獲取下一個可用工作隊列索引)、“AddWorkQueueEntry”(添加工作隊列條目)、“GetNextWorkQueueEntry”(獲取下一個工作隊列條目)和“MarkQueueEntryCompleted”(標記隊列條目完成)等調用。但我們意識到,“GetNextAvailableWorkQueueIndex”其實不是必須的,因為索引完全可以在隊列內部管理,或者由調用方自己維護。這樣,我們可以將接口精簡到三個:“AddWorkQueueEntry”、“GetNextWorkQueueEntry”和“MarkQueueEntryCompleted”。

更進一步,我們發現還可以優化這些接口。觀察到“MarkQueueEntryCompleted”和“GetNextWorkQueueEntry”這兩個操作通常是成對出現的——完成一個任務后,通常會接著獲取下一個任務。我們突發奇想,能否將這兩個功能合并成一個單一的調用,比如叫做“complete and get next”(完成并獲取下一個)。這個新函數的邏輯是:標記當前任務為已完成,同時返回隊列中的下一個任務條目。這樣不僅減少了函數調用的次數,還讓代碼的邏輯更加緊湊和流暢。

我們還考慮了隊列的數據結構設計。一種替代方案是使用虛空指針(void pointers)來存儲隊列中的條目,這樣可以讓隊列更加通用,適應不同的任務類型。不過,目前的簡化方向已經讓我們很滿意。通過將接口精簡到三個,甚至進一步合并為一個“complete and get next”的調用,我們顯著降低了系統的復雜性,同時保持了足夠的靈活性。

總的來說,我們對現在的設計方向感到滿意。這個抽象隊列既能滿足游戲代碼的需求,又能在性能和簡潔性之間找到平衡。接下來,我們可能會繼續調整細節,比如確定是否真的需要內聯,或者進一步驗證合并調用在實際運行中的效果。但目前來看,這個方案已經非常優雅且實用,我們很喜歡這個優化后的結果。

將 GetNextWorkQueueEntry 重命名為 CompleteAndGetNextWorkQueueEntry,并使其接受 work_queue_entry Completed 參數。

我們現在考慮如何把工作隊列的操作簡化,減少調用的次數,讓整個系統更高效。以下是詳細的總結:

我們覺得可以用一個更簡單的方案來優化當前的隊列操作。我們最初的想法是,當調用 GetNextWorkQueueEntry(獲取下一個工作隊列條目)時,可以直接改成一個新的函數,叫做 CompleteAndGetNextWorkQueueEntry(完成并獲取下一個工作隊列條目)。這個新函數的作用是把“標記完成”和“獲取下一個條目”兩個步驟合在一起,這樣就不用分開調用兩次了。

具體來說,這個新函數的工作方式是這樣的:我們會傳入一個工作隊列條目(work_queue_entry),表示當前已經處理完的任務。函數會先檢查這個傳入的條目是否有效(比如用 completed 判斷)。如果有效,就對隊列的完成計數(completion count)做一個線程安全的遞增操作(interlock increment),把這個條目標記為已完成。然后,函數會繼續從隊列中取出下一個待處理的條目,并把它返回給我們。

這樣設計的實際效果是,傳入一個完成的條目,函數就處理完它,然后直接給我們下一個要做的任務,整個過程一步到位。我們覺得這個操作很像“一個進,一個出”的模式——完成一個任務,馬上拿到下一個任務,減少了中間的步驟和調用次數。

我們認為這個方案很不錯,因為它簡化了流程,看起來也挺優雅。如果我們把代碼改成這樣,邏輯會更清晰。比如,假設傳入的條目是有效的,函數會先遞增完成計數,更新隊列狀態,然后再返回新的條目。如果條目無效,可能就直接返回空或者某種默認值,具體細節可以再調整。

為了實現這個想法,我們打算重寫一部分代碼,把現在的隊列操作改成主要依賴 CompleteAndGetNextWorkQueueEntry。這樣,整個系統可能只需要兩個核心函數:一個是 AddWorkQueueEntry(添加工作隊列條目),用來往隊列里放任務;另一個就是這個新的 CompleteAndGetNextWorkQueueEntry,用來完成任務并獲取下一個。我們覺得這種“一個進,一個出”的方式很像軍隊里的流程,簡單直接又高效。

總的來說,我們對這個優化方向很滿意。它不僅能減少調用次數,還讓代碼邏輯更流暢。接下來,我們會動手改代碼,試試這個新函數在實際運行中怎么樣,看看能不能達到預期的效率和穩定性。
在這里插入圖片描述

略微調整 ThreadProc 函數。

我們現在決定稍微調整一下代碼的結構,特別是針對 DoWorkerWork(執行工作者任務)這個函數的邏輯。我們希望讓它更符合之前提到的 CompleteAndGetNextWorkQueueEntry(完成并獲取下一個工作隊列條目)的設計思路。以下是詳細的總結:

我們計劃把 DoWorkerWork 的邏輯重新整理一下,主要是想把獲取下一個隊列條目的部分獨立出來。我們覺得,與其讓這個函數自己處理所有的隊列操作,不如直接利用 CompleteAndGetNextWorkQueueEntry 來完成任務的切換和獲取,這樣代碼會更簡潔,也更符合我們之前的優化目標。

調整后的邏輯是這樣的:我們先定義一個工作隊列條目(work_queue_entry),把它叫做 entry,并且初始化為零,表示還沒有任何任務。然后,我們調用 CompleteAndGetNextWorkQueueEntry,把這個 entry 和隊列(queue)一起傳進去。函數會處理傳入的 entry(如果它代表一個已完成的任務,就標記完成),然后返回一個新的 entry,替換掉原來的內容。我們覺得這個過程非常直白:傳入舊的條目,拿到新的條目,一步到位。

這樣做的好處是我們不用再分開調用兩次函數。以前可能需要先標記完成,再獲取下一個條目,現在只需要調用一次 CompleteAndGetNextWorkQueueEntry,就同時完成了這兩個步驟。我們覺得這讓我們擺脫了重復調用的麻煩,代碼更高效了。

接下來,我們會根據返回的 entry 來決定下一步。如果這個新的 entry 是有效的(比如通過某種 Entry.IsValid 判斷),我們就繼續處理它;如果無效,就說明隊列里沒有任務了,我們就停下來。具體來說,如果 entry 有效,我們會調用 DoWorkerWork 來執行任務。不過,我們注意到,調整后的 DoWorkerWork 不再需要直接接收隊列(queue)作為參數,因為隊列操作已經交給 CompleteAndGetNextWorkQueueEntry 處理了。我們只需要把新的 entry 傳給 DoWorkerWork,可能還要加上邏輯線程索引(ThreadInfo->LogicThreadIndex),用來標識當前線程的上下文。

我們覺得這樣的安排更合理。DoWorkerWork 的職責被簡化成了純粹的任務處理,不用再關心隊列的管理。而 CompleteAndGetNextWorkQueueEntry 負責所有的隊列交互,包括標記完成和獲取新任務。我們還考慮把邏輯線程索引作為一個參數保留下來,因為它可能對線程特定的操作有幫助,雖然具體用處可以再細化。
在這里插入圖片描述

調整 DoWorkerWork 函數。

我們現在決定進一步調整 DoWorkerWork(執行工作者任務)這個函數的實現方式,讓它更簡潔、更專注。以下是詳細的總結:

我們覺得,與其讓 DoWorkerWork 接收整個隊列(queue)作為參數,不如直接只傳入具體的任務信息。這樣可以讓函數的職責更明確,不用去處理隊列相關的操作。我們計劃修改它的參數,只傳遞兩個東西:一個是工作隊列條目(work_queue_entry),也就是當前要處理的任務;另一個是邏輯線程索引(LogicThreadIndex),用來標識當前線程的上下文。

調整后的邏輯是這樣的:我們假設調用 DoWorkerWork 之前,已經通過 CompleteAndGetNextWorkQueueEntry 獲取了一個有效的條目(entry)。為了確保這一點,我們會在函數開始時加一個斷言(assert),檢查傳入的 entry 是否有效。如果斷言通過,就說明我們拿到了一個合法的任務條目,可以直接使用它來執行具體的任務處理。

我們還決定讓 DoWorkerWork 不返回任何值。之前可能考慮過返回某種狀態或者結果,但現在我們覺得沒必要。因為這個函數的主要目標是處理任務,任務完成與否已經通過隊列的計數更新(比如 EntryCompletionCount)反映出來了,函數本身只需要專注執行就行。這樣,它就變成了一個純粹的“執行者”,不需要額外的返回值來增加復雜性。

具體來說,新的 DoWorkerWork 會是這樣:接收一個 work_queue_entry 和一個 LogicThreadIndex,先斷言條目有效,然后直接根據這個條目里的信息執行任務。因為隊列的管理已經被 CompleteAndGetNextWorkQueueEntry 處理好了,我們不需要再傳入整個隊列對象,代碼變得更簡潔,職責也更清晰。

我們覺得這個調整很合理。通過去掉隊列參數,函數的接口更輕量,我們也能更專注于任務本身的處理邏輯。邏輯線程索引保留下來,是因為它可能在某些線程特定的操作中還有用,雖然具體用途可以后續再細化。總的來說,這個改動讓我們對整個工作流程的控制更明確,接下來我們會把這個新版本實現出來,看看實際運行效果如何。

在這里插入圖片描述

微調 QueueWorkStillInProgress 循環。

我們計劃在代碼的下半部分實現一個循環,用來持續處理隊列中的任務。具體來說,這個循環會先定義一個工作隊列條目(entry),用來存儲當前的任務信息。然后,我們會調用之前提到的 CompleteAndGetNextWorkQueueEntry(完成并獲取下一個工作隊列條目),把當前的 entry 傳進去,獲取一個新的 entry,也就是下一個待處理的任務。

循環的邏輯是這樣安排的:每次調用 CompleteAndGetNextWorkQueueEntry 后,我們會檢查返回的新 entry 是否有效(比如通過某種 Entry.IsValid 判斷)。如果這個新條目是有效的,我們就進入一個處理分支,直接用這個條目來執行任務。這個處理分支的邏輯和之前的循環非常相似,幾乎是一模一樣的,只不過現在的實現更簡潔。關鍵區別在于,如果新條目無效,循環不會繼續等待,而是直接退出,結束當前的任務處理流程。只有當有有效任務時,它才會繼續運行,執行需要做的事情。

我們覺得這樣的設計很自然。循環的核心就是不斷地獲取新任務,只要有任務就處理,沒有任務就停下來。這樣,代碼既能保持高效,又能適時退出,避免無意義的空轉。我們還注意到,這個循環會依賴之前調整好的 DoWorkerWork 函數,因為它現在只接受 entry 和邏輯線程索引(logical thread index)作為參數,專注于任務執行,不用再管隊列本身的管理。
在這里插入圖片描述

在這里插入圖片描述

好像有問題主線程沒執行

在這里插入圖片描述

如果線程的任務執行完成遞增EntryCompletionCount 就行
如果Queue->EntryCount != Queue->EntryCompletionCount認為任務還在處理中

還是有段錯誤不知道什么原因導致的

在這里插入圖片描述

如果無效的話不讓執行DoWorkerWork
在這里插入圖片描述

在這里插入圖片描述

編譯并考慮刪除一個額外的調用。

我們現在討論的是一個隊列系統的簡化設計,只需要暴露三個核心功能就能讓這個隊列正常工作。目前我們確定的功能包括:AddWorkQueueEntry、CompleteAndGetNextWorkQueueEntry,以及CompleteAndGetNextWorkQueueEntry。不過,我們進一步思考后覺得,其實可以再精簡,甚至可以把其中一個功能去掉,最終只保留兩個功能調用——AddWorkQueueEntry和GetNextAvailableWorkQueueIndex。

我們還考慮了一些更奇怪但可能有趣的優化方案。比如,可以讓某個功能接受一個指針作為參數,這樣就不需要額外調用另一個功能來處理某些情況。我們之所以在這里猶豫不決,是因為我們希望盡量減少不必要的函數調用。當前設計中有一個功能,它的主要作用是防止外部計數和內部實際計數出現不同步的情況。但我們覺得這個功能其實不是必須的,去掉它可以讓整個系統更簡潔。

我們反復權衡的原因是希望確保系統的設計既高效又可靠。雖然可以用額外的函數來避免計數不同步的風險,但如果能通過其他方式規避這個問題,這個函數調用就顯得多余了。我們傾向于讓設計盡量精簡,同時保證隊列的核心功能不受影響。現在的想法是,通過調整設計,可能只需要兩個主要的函數調用就能完成所有必要的操作,這樣不僅減少了復雜性,也降低了出錯的可能性。

繼續進行,并將 work_queue 拆分成兩個獨立的部分。

準備對這個工作隊列的實現進行調整和優化。我們一開始的想法是保留一些擴展功能,因為我們感覺未來可能會需要更多特性。所以,我們考慮把工作隊列分成兩個部分:一個基本的隊列結構,另一個是指向外部緩沖區的指針,這些緩沖區可以用來描述具體的工作任務。這樣設計的優點是靈活性更高,隊列可以支持各種類型的任務。

我們進一步討論后覺得,當添加工作隊列條目時,可以直接傳遞一個指針,這個指針本身就包含了任務的具體信息。這樣既能保持設計的簡潔,又能告訴系統這個條目到底是什么。我們決定暫時讓這個概念保持簡單,但同時保留外部操作的可能性,也就是說,隊列本身不負責所有細節,而是通過指針指向外部數據。

接著,我們重新審視了之前打算移除的一個功能,決定把它恢復回來。這個功能涉及一個用戶指針(UserPointer),用來和隊列條目關聯。我們覺得每個隊列應該有一定數量的條目,比如256個,作為隊列的大小。我們一開始考慮用這個數字來控制隊列的容量,但后來意識到,可以通過指針直接管理數據,這樣就不需要額外的“下一個條目”(next entry)計數了。添加新條目時,只需要把指針寫入隊列,然后遞增索引即可。

為了確保隊列不會溢出,我們加入了一個斷言(assertion),檢查是否有足夠的空間來存儲新指針。我們暫時不打算讓隊列變成循環緩沖區(rolling buffer),而是先保持簡單設計。這個work_queue_entry_store其實就是一塊用來存放指針的區域。我們最初還想為每個條目定義具體的數據結構,比如傳入一個void*類型的指針來表示條目數據,但后來發現其實沒必要這么復雜。

我們繼續簡化設計,意識到既然指針本身就能表示數據,就不需要單獨的條目數據字段了。比如,如果任務數據是一個字符串,我們直接用指針指向這個字符串即可,不需要額外的封裝。這樣一來,添加條目時只需要傳入字符串的指針,隊列就知道如何處理。完成任務時,我們通過索引從隊列中取出對應的指針,獲取數據,然后遞增索引,邏輯非常直截了當。

最終,我們把整個設計精簡到只需要基本的操作:推送時直接傳入字符串指針,完成時根據索引取回數據并更新隊列狀態。這樣,系統中幾乎沒有多余的結構或函數,所有的操作都圍繞指針直接展開。我們覺得這個方案已經足夠簡單高效,滿足當前需求,同時也為未來可能的擴展留下了空間。總結來說,我們的目標是通過最少的代碼和結構,實現一個可靠且易用的工作隊列。
在這里插入圖片描述

再次運行。

現在有了這個基礎,我們感覺幾乎可以隨心所欲地擴展功能,一切應該都會運行得很順利。不過,我們也注意到一個問題:目前的設計還沒有提供重置隊列的方法。如果隊列滿了,我們可能會觸發之前設置的斷言(assertion),但我們并不太擔心,因為我們的計劃是把這個隊列改造成一個循環緩沖區(rolling buffer)。到時候如果遇到問題,我們再處理也不遲,畢竟循環緩沖區是我們未來的目標。

game_platform.h: 將這些函數提升到頭文件中。

我們梳理了一下,目前有三個核心函數需要實現和調用:AddWorkQueueEntry、MarkQueueEntryCompleted,以及CompleteAndGetNextWorkQueueEntry。除此之外,還有一個QueueWorkStillInProgress。我們覺得這基本上已經涵蓋了所需的一切,至少從目前來看是這樣。

我們反復檢查了一遍,覺得這些功能應該足夠支撐系統的運轉。實現的方式有幾種選擇,我們還沒完全確定到底用哪一種最好。我們考慮了一些替代方案,比如是否可以再做些調整或優化,但最終覺得現在的設計已經夠用了,沒必要再增加復雜性。我們討論了一下其他可能的做法,但很快就否決了,因為覺得多余的改動可能會適得其反。

總的來說,我們對現在的方案挺滿意的。這三個函數加上進行中的狀態,邏輯清晰,覆蓋了基本需求。我們打算就這樣開始推進,先把這些功能部署出去,看看實際運行效果。如果有需要,再根據情況調整。現在的重點是盡快上線,確保系統穩定運行,其他的細節可以邊用邊完善。
在這里插入圖片描述

多思考一下。

好的,我們決定再多思考一下這個設計,看看還能不能進一步優化。我們一邊討論一邊覺得,如果繼續簡化這個系統,也許可以把它變成一個純粹的分發隊列(dispatch queue)。我們的想法是這樣的:工作隊列的條目只需要包含兩個信息——要調用的對象(比如“誰來處理”),以及交給這個對象的一組數據(就像一個數據包)。如果按照這個思路走,整個系統可能會變得更簡單。

我們分析了一下,如果真的這么設計,可能只需要保留一個核心函數:添加工作隊列條目(AddWorkQueueEntry)。然后再加一個函數,這個函數實際上是把當前的一些操作合并起來的。我們想象這個合并的函數有點像是一個循環操作的組合——它既能完成任務,又能獲取下一個任務的功能合二為一。我們在腦海中過了一遍這個流程:添加條目后,這個組合函數會直接處理分發、執行和取下一個條目的邏輯。

我們之所以會想到這個方向,是因為覺得現在的三個函數(添加、完成、獲取下一個)雖然已經很精簡,但還是有些重復的步驟。如果能把后兩者合并成一個操作,系統的調用會更少,邏輯也更緊湊。我們一邊討論一邊嘗試理清思路,覺得這個“分發隊列”的概念挺有潛力。每個條目只負責指明“誰”和“做什么”,然后由一個統一的函數來驅動整個流程,這樣既直觀又高效。

不過,我們還在摸索具體的實現方式。比如,這個組合函數到底怎么寫,才能既簡潔又不失靈活性?我們暫時還沒完全想清楚,但覺得這個方向值得一試。接下來,我們可能會先試著把這個想法落實下來,看看實際效果如何。如果可行的話,整個隊列系統就只需要兩個函數調用,結構會更加扁平,我們對此還是挺期待的。

game_render_group.cpp: 先編寫使用代碼。

我們現在決定先快速切換思路,把另一部分的代碼寫出來。我們覺得在深入優化之前,先寫出使用代碼(usage code)會更有幫助,因為這樣可以直觀地看到整個系統的調用方式是什么樣的。我們想通過這種方式驗證設計的合理性,所以準備模擬一個場景來測試。

我們假設要處理的任務是多線程的,比如一個瓦片渲染(tile render)的任務。我們提前知道瓦片的數量,這讓我們可以設計一個簡單的結構來表示工作。我們先考慮一個叫“tile_render_work”的結構,里面可以包含一些基本信息,比如瓦片的X和Y坐標。不過我們馬上想到,其實只需要一個索引(index)就能推算出具體瓦片的位置,所以沒必要存太多數據。但為了展示更復雜的情況,我們故意讓這個結構稍微復雜一點,假設任務需要傳入一堆參數,而不僅僅是一個簡單的索引。

我們的想法是把這個系統當作一個完整的分發隊列(dispatch queue)來處理。我們設計了一個函數,比如“DoTiledRenderWork”,它接受一個數據指針(data),然后通過類型轉換(cast)識別出這是一個“TiledRenderWork”類型的數據,進而執行具體的渲染邏輯。我們覺得這個函數可以直接包含具體的操作代碼,而不是僅僅作為一個分發入口。為了徹底驗證這個思路,我們決定直接把渲染邏輯寫進去,比如計算瓦片位置并執行相關操作。

目前我們還不打算實現真正的多線程(hyper-threading),而是先用單線程把所有瓦片處理一遍,模擬兩組不同的任務。我們假設有一堆待處理的瓦片(stock of work),可以用一個數組來存儲這些任務數據。瓦片數量是已知的,比如X方向和Y方向各有多少個,我們可以通過簡單的乘法得出總數(TileCountX × TileCountY)。這樣,我們就可以提前分配好任務隊列的內容。

在具體實現時,我們準備先定義這個任務堆棧(stack),然后通過循環把每個瓦片任務加入隊列。我們設想每個任務都包含足夠的信息,比如瓦片的具體位置,然后分發函數會根據這些信息執行操作。我們一邊討論一邊調整,確保邏輯清晰。比如,我們可以用一個簡單的索引來遍歷所有瓦片,然后通過計算得出每個瓦片的坐標。雖然現在是單線程,但我們覺得這個設計將來稍微改動就能支持多線程。
在這里插入圖片描述

編譯并對 const 表示不滿。

有時候,面對編程中的一些規定和規則,感覺非常煩人。比如說,明明編譯器在某種情況下能接受某個表達式,但在另一些情境下卻完全不能理解。這種不一致性讓人很困惑,尤其是在一些簡單的語法上。例如,原本一個詞可以直接省略,但在某些上下文中卻必須寫得非常明確,像是“const”和“cost”之類的。顯然,“const”表示常量,這本身是顯而易見的,但為了避免歧義和確保正確性,卻必須明確標明。這種看似多余的要求令人覺得非常無奈和煩躁。

而且,尤其是在一些特殊情況下,某些寫法會被接受,另一些則不行。盡管這些規則本身并沒有錯,但從用戶的角度看,這些要求顯得有些繁瑣且不夠直觀。畢竟,編程的目的是解決問題,而不是讓這些規則變得更加復雜,甚至讓人感覺它們本身才是問題的根源。這種繁復的語法和格式要求讓人感覺像是一直在被限制,不能自由地發揮。

因此,盡管能夠通過這些規則來實現一些效果,但依然覺得這種強制要求非常令人沮喪,并且希望能有更為直觀和簡化的方式來處理這些編程問題。

完成編寫 TiledRenderGroupToOutput 函數。

在實現這個過程時,首先,我們需要設定一個初始的工作索引(WorkCount)為零。然后,對于每一項任務,我們可以將其添加到工作數組中,類似于 WorkArray,并且每次迭代時都更新該索引,直到所有工作都被列出并加入隊列。

在執行過程中,我們需要將任務添加到工作隊列中,這個隊列的類型是渲染隊列(RenderQueue)。每次添加任務時,需要指定具體的渲染工作和相關的參數。通過調用函數 AddWorkQueueEntity,我們將這些工作條目傳遞到隊列中,其中會涉及到具體的渲染任務函數(如 DoTiledRenderWork),以及相應的指針信息。這些信息會幫助我們指明在任務隊列中需要處理的具體內容。

接著,我們可以通過一種方式來管理和處理隊列中的工作,利用 PlatformComplateWorkQueue 來處理和完成所有的任務。通過調用如 PlatformComplateWorkQueue 這樣的函數,我們可以確保在渲染隊列中的所有工作都得以順利執行。具體來說,可以通過設置適當的函數指針來管理隊列中的任務,從而實現隊列的管理和工作項的執行。

一旦所有工作都添加到隊列中,并且完成了相應的處理,最終的渲染任務會依次完成。在這個過程中,系統會繼續迭代工作隊列,直到所有任務都處理完畢。對于每一輪的工作,我們都要確保相關的渲染任務已經準備好并傳遞到隊列中,確保渲染工作順利進行。

在編譯時,如果隊列還沒有完全實現或者某些部分還未完成,系統可能會處于一個編譯錯誤狀態,因此在開發過程中需要不斷調試,確保每個部分正確地鏈接并處理。最終,通過調整工作索引和確保工作隊列的執行,我們可以逐步完成目標任務,并最終在渲染隊列上實現預期的效果。

最后,在實際運行時,可能會遇到一些沒有立即可見結果的情況,特別是在調試階段。此時,需要繼續優化和檢查隊列中的每一項任務,以確保整個渲染過程能夠按預期順利進行。
在這里插入圖片描述

編譯并運行,程序崩潰。

在這里插入圖片描述

回顧并展望多線程的未來。

現在,渲染工作終于開始順利進行。所有需要的部分都已經準備好了,我們也搞清楚了相互之間的連接。接下來,我們只需要將這些部分連接起來,明天就能開始將它們整合到一起。這一切聽起來非常順利和令人興奮。

一旦將這些部分連接起來,我們將能夠實現一個通用的工作隊列。通過這個工作隊列,我們可以傳遞多個任務隊列,并將任務分配到不同的隊列中。這樣一來,我們可以持續不斷地將任務提交到隊列,系統就可以在后臺處理這些任務了。

例如,可以為后臺創建一個任務隊列,專門處理一些較重的計算任務,比如瓦片的渲染(tile rendering)。通過這種方式,系統能夠高效地處理這些任務,而不會因為大量任務的堆積而影響其他操作。整個流程看起來非常強大,能夠不斷地從隊列中提取任務并分配給合適的線程或模塊處理。

這種架構不僅提高了任務處理的效率,也為系統提供了更好的靈活性。通過任務隊列,可以將不同的工作分配到合適的時間和位置,讓渲染過程更加流暢和高效。這種設計方式非常適合需要大量并行計算的任務。

你會為每個隊列創建新線程嗎?

我們不會為每一個工作單獨創建新線程,而是打算以一種更加靈活的方式來處理線程的分配和任務調度。雖然系統需要處理多個任務,但我希望能設計出一種機制,讓線程能夠在不渲染的時候執行其他任務。這樣,線程在渲染任務之外的時間就可以去做一些其他工作,提高效率。

具體來說,我們的目標是通過設置多個任務隊列來管理工作流。在渲染時,我們希望將所有資源集中在一個隊列上,確保渲染任務能夠及時完成。所以,在渲染的關鍵時刻,系統只會從一個隊列中提取任務,確保所有的計算和渲染都能在規定時間內完成。

而當沒有渲染任務時,系統就有更多的靈活性。線程可以去處理其他不需要立即完成的任務。這就需要一個背景隊列,用于存儲那些不急需立即完成的工作。這樣,當系統處于非渲染階段時,可以把線程的計算力用于這些“后臺工作”。

總的來說,系統將有兩個主要的隊列:一個是渲染隊列,專門用來處理需要在特定時間內完成的渲染任務,另一個是后臺工作隊列,用于處理不急于完成的任務。通過這種方式,系統能夠在保證實時渲染任務完成的同時,充分利用空閑時間來處理其他任務。

仍然不理解 volatile 和內存屏障的用法。

今天正好有時間,可以詳細解答一些關于多線程編程的問題,尤其是 volatile 和內存屏障(memory barrier)這兩個概念。其實這兩個概念在多線程編程中非常重要,它們主要涉及到線程之間的內存可見性和順序性問題。

首先,volatile 關鍵字的作用是告訴編譯器,不要對標記為 volatile 的變量進行優化。它確保每次讀取這個變量時,都是從內存中獲取,而不是使用緩存或寄存器中的值。這樣,多個線程訪問該變量時,每個線程都能看到最新的值。但是,volatile 并不能解決線程之間的同步問題,它僅僅確保了變量的可見性,但不能控制操作的順序,也不能避免競態條件。

接下來是內存屏障(memory barrier),它是一種指令,用于控制特定操作的順序,避免指令重排序帶來的問題。在多線程程序中,處理器和編譯器可能會對指令進行優化和重排序,這可能會導致某些變量在多線程環境下出現不一致的情況。內存屏障可以強制執行特定操作的順序,確保在執行某個操作之前,其他相關的操作已經完成。

簡單來說,volatile 只是保證了變量的可見性,而內存屏障則更關注操作的順序性,它們在一起使用時,可以確保多線程環境下的內存訪問是安全的。不過,volatile 和內存屏障并不能替代鎖機制或其他同步工具,它們只是確保數據一致性的一個輔助工具。

這兩個概念在多線程編程中起著至關重要的作用,理解它們的工作原理,可以幫助更好地管理線程間的交互和數據一致性。

黑板:內存和代碼屏障。

問題的關鍵在于如何確保多線程環境下兩個操作的執行順序正確,避免數據不一致或程序崩潰。我們有兩項關鍵操作:

  1. 設置數據:將數據放入隊列中(entry.data = data)。
  2. 增加計數:增加隊列中的計數器(entry_count++)。

在這里,有一個隱含的要求:當entry_count增加時,其他線程可能會檢查這個計數器,如果它被增加了,那么它們就會開始處理新的任務。然而,我們必須確保數據的寫入操作entry.data = data)在計數器更新操作entry_count++)之前完成,否則其他線程可能會看到無效的數據,從而導致錯誤的行為。

這個問題的第一層復雜性來自編譯器。在C++代碼中,這兩條指令是獨立的,編譯器并不知道我們希望它們按照特定順序執行。編譯器可能會為了優化,將它們重新排序,可能先執行entry_count++,再執行entry.data = data。這樣一來,雖然代碼看似沒有問題,但在執行時,數據的順序會被打亂,導致錯誤。

第一解決方案:編譯器屏障(Compiler Fence)

為了解決編譯器重排序的問題,可以使用編譯器屏障(編譯器屏障不會生成任何代碼,它只是一種標記,告訴編譯器不要重新排序這兩條指令)。可以通過 volatile 或特定的屏障指令來告訴編譯器,在這兩條指令之間不能有任何重排。通過在這兩條指令之間插入屏障,可以強制它們按正確的順序執行,避免出現問題。

第二解決方案:處理器層面的問題(Out-of-order Execution)

即使編譯器保證了指令順序正確,處理器仍然可能重新排序操作。現代處理器為了優化性能,允許指令在執行時“亂序執行”(out-of-order execution)。比如,處理器可能會決定先執行entry.data = data,然后再執行entry_count++,這可能會導致其他線程看到不一致的狀態。

為了解決這個問題,需要使用內存屏障指令(Memory Barrier)。這是一種指令,告訴處理器不要亂序執行,確保數據的寫入和讀取嚴格按順序執行。內存屏障有不同的類型:

  • 完整內存屏障(Full Fence):確保讀寫操作都不會被亂序。
  • 存儲屏障(Store Fence):確保寫操作不會被亂序。
  • 加載屏障(Load Fence):確保讀操作不會被亂序。

這些屏障指令的作用是讓處理器在執行時,確保內存操作的順序性,避免亂序執行帶來的問題。

綜合解決方案

要確保兩個操作按照正確的順序執行,我們需要:

  1. 在代碼中使用編譯器屏障,確保編譯器不會對這兩條指令進行重排序。
  2. 在匯編級別或者更底層的代碼中插入內存屏障,確保處理器不會重新排序操作。

如果編譯器和處理器都保證了操作順序,那么程序就可以按預期運行,不會發生線程間的競態條件或數據不一致的問題。

總結:

  • 編譯器屏障確保代碼順序不被重排序。
  • 內存屏障確保處理器執行時的順序性。
  • 這兩者的結合能夠有效解決多線程編程中的順序問題,確保數據的一致性和程序的穩定性。

這些技術通常不需要手動實現,因為許多同步原語(如互斥鎖、原子操作等)會自動處理這些問題,但在某些底層優化的情況下,開發者可能需要顯式地插入屏障指令。

是否可以移除 Entry.IsValid,改用測試 Entry.Data != NULL 來判斷?

可以將驗證數據有效性的部分移除,改為測試 Entry.Data != NULL。這是可行的,但需要對代碼進行一些調整。明天會對這部分代碼進行重新整理,因為之前在進行合并時做了一些修改。調整后的代碼需要確保在檢查數據有效性時,Entry.Data != NULL,這樣可以簡化邏輯,同時保證程序的正常運行。

k對 Naughty Dog 使用纖程(加上手動管理)和線程親和力綁定核心的做法怎么看,而不是使用經典的工作者/任務方法來實現多線程游戲玩法?

對于Naughty Dog使用纖程、手動內存管理和線程親和力(將線程綁定到特定核心)的方式,而不是采用經典的工作者任務方法來實現多線程游戲玩法,這種方法的具體實現我并沒有深入研究,因此沒有太多意見。

工作隊列是否可以接受任何函數并進行多線程處理?這個函數需要特別處理才能工作嗎?

這個方法可以適用于幾乎所有的函數,前提是這些函數在同時執行時不能發生沖突。例如,不能同時寫入同一個內存位置。以瓦片渲染為例,需要將任務分解成多個瓦片,以確保不同的任務之間不會發生沖突。只要函數不在并發執行時互相沖突,它就能正常工作。

請寫一個無鎖隊列,雖然我不知道那是什么,也不清楚你是否用過。

這是一個無鎖隊列,正如我們實現的那樣。雖然目前它還不完全符合傳統的無鎖隊列的定義,因為它還不是一個環形緩沖區,但最終我們會將它改成環形緩沖區,這樣它就會成為一個更標準的無鎖隊列。

創建線程會消耗多少 CPU 周期?或者更好的是:在兩個線程中工作來提高速度的最小周期數是多少?

線程的創建開銷其實是非常大的,尤其是在高負載的情況下。所以,我們的目標是避免每次都創建新線程。相反,我們希望在程序啟動時就創建好多個線程,并保持它們處于空閑狀態,隨時準備處理任務。這樣,當有任務需要處理時,線程可以立刻工作,而無需經歷啟動線程的額外開銷。通過這種方式,我們可以最大限度地減少多線程操作的開銷,從而提高性能。

為什么你叫它隊列,即使它可能是同時處理的?

即使多個工作可以同時進行,隊列的元素是按照提交的順序被取出的。具體來說,工作項被添加到隊列時,按照提交的順序進入隊列,而當工作項被取出時,也按照相同的順序被處理。這個過程保持著先進先出(FIFO)的規則,也就是隊列的傳統定義。因此,盡管隊列中的工作項可以并行處理,但隊列本身依舊是嚴格按照順序管理工作項的。

win32 文件中不就已經有一些與線程相關的代碼了嗎?

在之前的代碼中,確實有一部分與線程相關的內容,特別是能夠顯示當前線程的信息。但除此之外,暫時沒有更多與線程直接相關的實現。這部分可能會在以后進一步加入,或者也可能不進行修改。

你會添加一個酷炫的圖表,展示每個線程在每一時刻工作在什么任務上(例如來自哪個子系統)嗎?

計劃中會增加一個圖形化的調試工具,用于展示每個線程在每個時刻執行的任務,以及這些任務來自于哪個子系統。這是為了幫助可視化線程的工作狀態。雖然整個進度安排有些亂,實際上已經做了很多原本打算稍后再做的事情,這些準備工作會讓后續的任務變得更加輕松。比如,動畫部分會相對簡單,因為已經完成了紋理映射等先前的工作,所以可以更容易地進行資源的調度和處理。雖然進度順序有些變動,但目前的計劃依然會繼續推進,期待接下來的進展。

工作隊列中條目之間的偽共享是否可能對性能造成問題?

當多個線程同時訪問不同的工作隊列條目時,如果這些條目在內存中很接近,可能會出現偽共享問題。偽共享是指多個線程雖然訪問的是不同的內存位置,但由于緩存一致性協議的原因,這些內存位置被存儲在同一緩存行中,導致緩存行在多個處理器之間不斷地進行同步,從而引發性能問題。具體來說,如果多個線程操作的數據位于同一個緩存行,即使它們操作的數據并不沖突,也會因為緩存行的同步開銷導致性能下降。因此,合理地安排數據在內存中的布局,避免多個線程訪問同一緩存行,能夠減少偽共享的影響。

volatile 是否通過將匯編寄存器壓入棧中,然后再彈出恢復,來清除匯編寄存器?

volatile 關鍵字的作用是確保變量的值不會被優化掉,避免編譯器對其進行優化處理。其實現方式是將寄存器中的值保存到內存中,并在需要時重新加載。通常,這個過程并不涉及棧操作,而是將變量存儲在全局內存或堆中。通過這種方式,volatile 確保了每次訪問該變量時都會直接從內存中讀取最新的值,而不是使用寄存器中的緩存值。這種操作使得變量的值始終保持最新,避免編譯器在優化時將其值緩存,進而保證了多線程環境下的正確性。

為什么你想要編譯器屏障而不是進程屏障,反之亦然?

在一些平臺上,編譯器屏障(compiler fence)和處理器屏障(processor fence)的使用有不同的目的。一般來說,編譯器屏障是為了防止編譯器重新排列指令,從而確保代碼按照預期的順序執行。然而,在某些平臺上,比如 x86-64,編譯器屏障可能不太有用,因為處理器的內存寫入順序已經很強序(strongly ordered),這意味著處理器本身就會保證寫入順序。

因此,可能并不需要單獨使用編譯器屏障,因為處理器已經足夠強大,可以處理這些順序問題。而對于其他平臺,尤其是在處理器不保證強序時,編譯器屏障和處理器屏障可能會有更重要的作用。

總的來說,目前對編譯器屏障的理解可能還需要更多的研究,特別是對 x86-64 處理器的內存排序特性。在某些情況下,編譯器屏障可能沒有必要,具體情況需要深入了解平臺的內存模型。

_mm_fence() 不應該意味著編譯器屏障嗎?否則沒什么意義…

在討論 _mm_fence 和編譯器屏障時,存在一定的疑問。有些人認為,如果沒有處理器的屏障與編譯器屏障的結合,那么就沒有意義。但實際上,是否自動包含編譯器屏障,這一點并不確定。很多時候,最安全的做法是明確地加入 read-write barrier,即讀寫屏障,以確保數據的順序不會出現問題。

目前還沒有看到足夠的文檔說明 _mm_fence 是否一定會包括編譯器屏障,所以無法完全信任其行為。在這種情況下,為了避免潛在問題,最好顯式地在代碼中加入屏障,以確保線程間的正確同步。

你實現了摩擦力嗎?

已經實現了摩擦力的效果。我們的角色在移動時,受到類似摩擦阻力的影響,導致其速度逐漸減慢。這種效果模擬了摩擦力的存在,使得角色的運動看起來更自然,像是在一個有摩擦的環境中運動一樣。

所以,線程管理有點像內存管理(你希望提前設置它,而不是按需分配它)。

線程管理和內存管理有相似之處,主要在于我們希望提前設置好線程,而不是根據需求動態分配。就像內存分配一樣,創建一個線程在操作系統層面是非常昂貴的,雖然它的速度比較快,但在游戲規模的工作負載下,它依然會帶來較大的開銷。因此,通常的做法是先從操作系統獲取所需的線程數量,然后再進行子分配管理。

與內存管理類似,我希望能夠控制線程如何分配工作,而不是讓操作系統頻繁創建和銷毀線程,這樣會浪費大量時間。對于內存,我也不希望頻繁地從操作系統申請和釋放內存,而是希望自己管理內存的劃分,避免操作系統帶來額外的開銷。

會添加代碼來詢問處理器同時會執行多少線程嗎?

代碼中需要添加的部分是,用來檢查當前機器支持多少線程可以同時執行的功能。雖然還不確定具體如何添加,因為需要一個機制來判斷機器的情況,確保在某些特定情況下不要讓所有線程都處于休眠狀態,否則可能會導致問題。雖然可以先實現這個功能,但在實際操作中,可以先把它注釋掉,并強制設置線程數為8個,直到確定如何更好地處理這個問題。

是否有可能讓一個工作隊列條目生成另一個工作隊列條目?

目前工作隊列條目無法生成另一個工作隊列條目。原因是現在采用的是單生產者多消費者的隊列系統。如果希望一個工作條目能夠生成另一個工作條目,就需要將隊列改為多生產者單消費者模型,這會稍微復雜一些。因此,除非有必要,否則不打算這么做。如果以后發現必須要使用這種方式,再進行修改,但目前并不傾向于這樣做,因為這會增加一些額外的復雜性,可能不是最佳選擇。

(我并不是專家,所以才問這個問題)偽共享會導致處理器跳過緩存,當不同線程訪問同一緩存行時。

"偽共享"會導致處理器跳過緩存,特別是當不同的線程訪問同一緩存行上的數據時。偽共享問題通常出現在多個線程在同一個處理器核心上運行的情況。處理器無法知道兩個不同的線程是否訪問了同一個緩存行,直到緩存之間的協調發生。簡而言之,多個核心上的線程在處理共享數據時,處理器可能需要進行緩存一致性協議的交換。

不過,這個問題是否存在以及它在不同處理器架構上的表現可能有所不同。例如,在某些架構中(如AIX 64),處理器可能會通過緩存來處理這些數據,并且不會輕易跳過緩存。但在其他處理器上,這個過程可能更復雜,可能需要一些特定的優化來避免偽共享問題。

另外,根據某些文檔和錯誤報告,可能已經發現了一些優化方法,自動插入內存屏障(例如mfence)來解決這個問題。這意味著處理器可以通過更智能的處理來確保數據一致性,而無需手動管理每個內存操作的順序。

總的來說,偽共享可能在不同的硬件和架構上表現不同,需要根據具體的處理器特性和優化方式來進行調整。

其實,我能想到一種使用編譯器屏障而不使用內存屏障的場景:寫入 CPU 特殊寄存器,比如控制寄存器或 MSR。

在討論編譯器屏障(compiler fence)和內存屏障(memory fence)時,提到了一個可能的使用場景,那就是寫入特殊寄存器(如控制寄存器或MSR寄存器)。在這種情況下,編譯器屏障可能有其作用,而不需要涉及內存屏障。問題在于,是否這些操作算作"存儲"(store)操作。

通常,存儲屏障(store fence)是用于管理寫入緩存的操作,但寫入控制寄存器(例如MSR寄存器)是否被視為存儲操作就不那么明確了。對于這些寄存器的寫入操作是否會被存儲屏障阻止重排序,存在疑問。存儲屏障通常用于確保寫入到緩存的操作順序,而對特殊寄存器的寫入操作是否需要類似的機制,尚不清楚。因此,是否使用內存屏障來防止對這些特殊寄存器的寫入順序進行重排序,仍然是一個待解答的問題。

總結來說,對于控制寄存器的寫入是否需要存儲屏障,或者是否能夠通過編譯器屏障來控制這些寄存器的寫入順序,仍然是一個技術上的疑問,可能需要進一步的研究和驗證。

經過多年后重新回到 C/C++ 編程。不明白為什么你會混合使用 C 風格的結構體和 C++ 結構體?

重新回到C語言的編程中,發現符號(symbols)變得非常擁擠,這讓開發過程變得更加復雜。雖然不太明白為什么要將C語言風格的代碼和C++風格的代碼混在一起,但可以明確的是,我們的代碼中并沒有C++風格的結構體(struct)。實際上,整個項目中使用的都是C語言風格的結構體。因此,在編程時,我們保持了一致性,主要使用C風格的代碼和符號,而沒有引入C++的特性。

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

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

相關文章

藍橋杯 Java B 組之記憶化搜索(滑雪問題、斐波那契數列)

Day 5:記憶化搜索(滑雪問題、斐波那契數列) 📖 一、記憶化搜索簡介 記憶化搜索(Memoization) 是一種優化遞歸的方法,它利用 哈希表(HashMap)或數組 存儲已經計算過的結果…

反爬蟲策略

反爬蟲策略是網站用于防止自動化程序(爬蟲)惡意抓取數據的核心手段,其設計需兼顧有效性、用戶體驗和合法性。 一、 基礎檢測與攔截 User-Agent檢測:驗證請求頭中的User-Agent,攔截非常見或已知爬蟲標識。IP頻率限制&…

Java 實現快速排序算法:一條快速通道,分而治之

大家好,今天我們來聊聊快速排序(QuickSort)算法,這個經典的排序算法被廣泛應用于各種需要高效排序的場景。作為一種分治法(Divide and Conquer)算法,快速排序的效率在平均情況下非常高&#xff…

深入解析 Spring 中的 BeanDefinition 和 BeanDefinitionRegistry

在 Spring 框架中,BeanDefinition 和 BeanDefinitionRegistry 是兩個非常重要的概念,它們共同構成了 Spring IoC 容器的核心機制。本文將詳細介紹這兩個組件的作用、實現以及它們之間的關系。 一、BeanDefinition:Bean 的配置描述 1.1 什么…

《OpenCV》——光流估計

什么是光流估計? 光流估計的前提? 基本假設 亮度恒定假設:目標像素點的亮度在相鄰幀之間保持不變。這是光流計算的基礎假設,基于此可以建立數學方程來求解光流。時間連續或運動平滑假設:相鄰幀之間的時間間隔足夠小&a…

信息系統的安全防護

文章目錄 引言**1. 物理安全****2. 網絡安全****3. 數據安全****4. 身份認證與訪問控制****5. 應用安全****6. 日志與監控****7. 人員與管理制度****8. 其他安全措施****9. 安全防護框架**引言 從技術、管理和人員三個方面綜合考慮,構建多層次、多維度的安全防護體系。 信息…

如何進行OceanBase 運維工具的部署和表性能優化

本文來自OceanBase 用戶的實踐分享 隨著OceanBase數據庫應用的日益深入,數據量不斷攀升,單個表中存儲數百萬乃至數千萬條數據的情況變得愈發普遍。因此,部署專門的運維工具、實施針對性的表性能優化策略,以及加強指標監測工作&…

如何防止 Instagram 賬號被盜用:安全設置與注意事項

如何防止 Instagram 賬號被盜用:安全設置與注意事項 在這個數字化時代,社交媒體平臺如 Instagram 已成為我們日常生活的一部分。然而,隨著網絡犯罪的增加,保護我們的在線賬戶安全變得尤為重要。以下是一些關鍵的安全設置和注意事…

Redis|復制 REPLICA

文章目錄 是什么能干嘛怎么玩案例演示復制原理和工作流程復制的缺點 是什么 官網地址:https://redis.io/docs/management/replication/Redis 復制機制用于將數據從一個主節點(Master)復制到一個或多個從節點(Slave)&a…

對象存儲之Ceph

Ceph 對象存儲概述 Ceph 是一個開源分布式存儲系統,旨在提供高度可擴展、高度可用、容錯、性能優異的存儲解決方案。它結合了塊存儲、文件系統存儲和對象存儲的功能,且在設計上具有極高的可擴展性和靈活性。 在 Ceph 中,對象存儲&#xff0…

Document對象

DOM4j中,獲得Document對象的方式有三種: 1.讀取XML文件,獲得document對象 SAXReader reader new SAXReader(); Document document reader.read(new File("input.xml")); 2.解析XML形式的文本,得到document對象…

樹莓集團南京產業園再布局:深入剖析背后邏輯

在產業園區蓬勃發展的當下,樹莓集團在南京的產業園再布局行動備受矚目。這一舉措并非偶然,其背后蘊含著深刻且多元的戰略邏輯。 一、順應區域產業發展趨勢 南京作為長三角地區的重要城市,產業基礎雄厚且多元。近年來,南京大力推動…

Pytorch實現之腦電波圖像生成

簡介 簡介:采用雙GAN模型架構來生成腦電波與目標圖像。 論文題目:Image Generation from Brainwaves using Dual Generative Adversarial Training(使用雙生成對抗訓練的腦電波圖像生成) 會議:IEEE Global Conference on Consumer Electronics (GCCE) 摘要:表示通過無…

HTML解析 → DOM樹 CSS解析 → CSSOM → 合并 → 渲染樹 → 布局 → 繪制 → 合成 → 屏幕顯示

一、關鍵渲染流程 解析 HTML → 生成 DOM 樹 瀏覽器逐行解析 HTML&#xff0c;構建**DOM&#xff08;文檔對象模型&#xff09;**樹狀結構 遇到 <link> 或 <style> 標簽時會暫停 HTML 解析&#xff0c;開始加載 CSS 解析 CSS → 生成 CSSOM 將 CSS 規則解析為**…

劍指offer - 面試題11 旋轉數組的最小數字

題目鏈接&#xff1a;旋轉數組的最小數字 第一種&#xff1a;正確寫法&#xff08;num[m]和nums[r]比較&#xff09; class Solution { public:/*** 代碼中的類名、方法名、參數名已經指定&#xff0c;請勿修改&#xff0c;直接返回方法規定的值即可** * param nums int整型v…

Spring源碼分析の循環依賴

文章目錄 前言一、循環依賴問題二、循環依賴的解決三、整體流程分析 前言 常見的可能存在循環依賴的情況如下&#xff1a; 兩個bean中互相持有對方作為自己的屬性。 ??類似于&#xff1a; 兩個bean中互相持有對方作為自己的屬性&#xff0c;且在構造時就需要傳入&#xff1a…

Docker 部署 Jenkins持續集成(CI)工具

[TOC](Docker 部署 Jenkins持續集成(CI)工具) 前言 Jenkins 是一個流行的開源自動化工具&#xff0c;廣泛應用于持續集成&#xff08;CI&#xff09;和持續交付&#xff08;CD&#xff09;的環境中。通過 Docker 部署 Jenkins&#xff0c;可以簡化安裝和配置過程&#xff0c;并…

《Effective Objective-C》閱讀筆記(中)

目錄 接口與API設計 用前綴避免命名空間沖突 提供“全能初始化方法” 實現description方法 盡量使用不可變對象 使用清晰而協調的命名方式 方法命名 ?編輯類與協議命名 為私有方法名加前綴 理解OC錯誤模型 理解NSCopying協議 協議與分類 通過委托與數據源協議進行…

C++程序員內功修煉——Linux C/C++編程技術匯總

在軟件開發的宏大版圖中&#xff0c;C 語言宛如一座巍峨的高山&#xff0c;吸引著無數開發者攀登探索。而 Linux 操作系統&#xff0c;以其開源、穩定、高效的特性&#xff0c;成為了眾多開發者鐘愛的開發平臺。將 C 與 Linux 相結合&#xff0c;就如同為開發者配備了一把無堅不…

數據庫索引:缺點與類型全解析

在數據庫的世界里&#xff0c;索引就像是一本書的目錄&#xff0c;它能幫助我們快速定位到所需的數據&#xff0c;極大地提升查詢效率。然而&#xff0c;就如同任何事物都有兩面性一樣&#xff0c;索引也并非完美無缺。今天&#xff0c;我們就來深入探討一下索引的缺點以及常見…