解析:對(duì)象組件與資源管理的核心設(shè)計(jì))
1. 游戲?qū)ο笠胬锼袞|西的底層契約聊到游戲引擎我們可以把渲染、物理、動(dòng)畫都往后放一放有一個(gè)問(wèn)題必須最先回答游戲世界里千千萬(wàn)萬(wàn)個(gè)實(shí)體——角色、武器、草叢、掉落物、觸發(fā)區(qū)域——在代碼層面到底長(zhǎng)什么樣這就是游戲?qū)ο笙到y(tǒng)要解決的事。游戲?qū)ο驡ameObject/Entity本質(zhì)上不神秘。它就是一個(gè)ID加上掛在這個(gè)ID下面的一堆組件。一個(gè)角色的角色-ness來(lái)自哪里不是因?yàn)樗^承了一個(gè)叫Character的類而是因?yàn)樗砩蠏熘鳷ransform、MeshRenderer、AnimationController、Collider這些組件。組件模式的核心思想是組合優(yōu)于繼承這句話我已經(jīng)聽(tīng)膩了但真正在引擎里落地的時(shí)候你會(huì)發(fā)現(xiàn)它帶來(lái)的架構(gòu)收益遠(yuǎn)超預(yù)期。1.1 從層級(jí)樹(shù)到組件模式我們走了多遠(yuǎn)早年的引擎包括我剛開(kāi)始接觸游戲開(kāi)發(fā)那會(huì)兒的引擎普遍是用深繼承樹(shù)來(lái)表達(dá)對(duì)象的。所有東西都是ActorActor派生出PawnPawn派生出CharacterCharacter派生出HumanPlayer……每個(gè)品類都要在繼承樹(shù)里占一個(gè)位置。這種設(shè)計(jì)在品類少的時(shí)候很順手但項(xiàng)目一大就原形畢露一個(gè)能飛行的載具需要派生自Vehicle可同時(shí)它又想掛載武器系統(tǒng)武器系統(tǒng)在另一棵樹(shù)上——總不能搞多重繼承吧那鉆石繼承的噩夢(mèng)比游戲本身還恐怖。組件模式換了個(gè)思路實(shí)體本身只是一個(gè)空殼行為全部由可插拔的組件提供。飛行能力就是一個(gè)IMovementComponent的實(shí)現(xiàn)載具底盤是一個(gè)VehicleChassis組件武器是WeaponComponent。你想做一個(gè)會(huì)飛的武裝載具把兩個(gè)組件往同一個(gè)Entity上一掛就完事了連類都不用新建。我維護(hù)的自研引擎在2018年做過(guò)一次徹底的重構(gòu)從層級(jí)樹(shù)切到純組件模式。那次重構(gòu)之后新玩法原型從需要寫一個(gè)新類再改一堆工廠方法變成了在配置文件里寫幾行組件列表。這個(gè)體驗(yàn)差異是巨大的策劃同學(xué)自己都能拼出一個(gè)新單位程序員只需要保證組件的功能完整就行。1.2 ECS數(shù)據(jù)導(dǎo)向的組合演進(jìn)組件模式解決了代碼組織的靈活性但它沒(méi)有解決性能問(wèn)題。一個(gè)典型的MMO場(chǎng)景里可能有上萬(wàn)的對(duì)象在跑如果用面向?qū)ο蟮姆绞浇M織組件——每個(gè)組件一個(gè)獨(dú)立的堆分配對(duì)象通過(guò)指針互相引用——那cpu緩存基本處于報(bào)廢狀態(tài)。你遍歷1000個(gè)角色的位置信息內(nèi)存訪問(wèn)是到處跳的Cache Miss率能把幀時(shí)間拖垮三分之一。ECSEntity-Component-System把組件從掛在對(duì)象上的成員改成了連續(xù)排列的結(jié)構(gòu)體數(shù)組。同一類型的組件塞進(jìn)同一個(gè)數(shù)組遍歷的時(shí)候直接線性讀內(nèi)存配合現(xiàn)代CPU的預(yù)取機(jī)制速度能提升一到兩個(gè)數(shù)量級(jí)。加上System的批處理語(yǔ)義一幀內(nèi)對(duì)同一類型組件的操作天然就是SIMD友好的。但這不代表所有項(xiàng)目都要上ECS。我和朋友聊過(guò)很多次如果你的對(duì)象總數(shù)不超過(guò)5000也不做大規(guī)模物理模擬或?qū)ぢ穫鹘y(tǒng)的組件模式足夠用而且調(diào)試起來(lái)直觀得多。ECS最大的代價(jià)是思維方式的轉(zhuǎn)變你不能再用這個(gè)對(duì)象現(xiàn)在處于什么狀態(tài)來(lái)思考而要習(xí)慣這幀有哪些System處理了哪些組件。引擎選型從來(lái)不是選最好的是選你項(xiàng)目規(guī)模最合適的。1.3 對(duì)象的生命周期創(chuàng)建、激活、銷毀、切換游戲?qū)ο蠊芾碜钊菀妆缓鲆暤肿钊菀妆椎氖巧芷?。一個(gè)角色死亡之后他身上的AI組件要先停止響應(yīng)渲染組件要播放消散效果物理碰撞體要被移除最后才是回收內(nèi)存。如果直接把對(duì)象銷毀了效果還沒(méi)播完資源就釋放了畫面直接閃瞎眼。我常用的方案是給對(duì)象做四態(tài)管理未初始化、激活、失活、待銷毀。未初始化只表示對(duì)象池里被拿到了一塊內(nèi)存數(shù)據(jù)還沒(méi)填激活態(tài)才參與系統(tǒng)的幀更新失活態(tài)保留數(shù)據(jù)但退出所有系統(tǒng)待銷毀則是延遲一幀再真正回收保證渲染管線正在用的數(shù)據(jù)不會(huì)半路被干掉。對(duì)象池這塊我也多說(shuō)一句。千萬(wàn)不要在戰(zhàn)斗激烈的時(shí)候頻繁new和delete對(duì)象那是給GC和堆分配器上強(qiáng)度。池的初始容量看場(chǎng)景峰值的八成不夠再擴(kuò)容擴(kuò)容按倍率走。每個(gè)對(duì)象還留一個(gè)generation序號(hào)池里復(fù)用時(shí)序號(hào)自增任何持有舊引用的代碼在訪問(wèn)時(shí)核對(duì)序號(hào)對(duì)不上就說(shuō)明對(duì)象已經(jīng)被復(fù)用了直接安全拒絕訪問(wèn)。這個(gè)設(shè)計(jì)幫我在線上查過(guò)不少幽靈Bug。2. 資源管理游戲資產(chǎn)的物流中樞游戲?qū)ο蠼鉀Q的問(wèn)題是運(yùn)行時(shí)這個(gè)世界有哪些實(shí)體資源管理解決的問(wèn)題是這些實(shí)體需要的資產(chǎn)從哪來(lái)、怎么到、何時(shí)走。很多人把這兩件事分開(kāi)看但實(shí)際項(xiàng)目中它們是強(qiáng)綁定的。一個(gè)角色對(duì)象在場(chǎng)景中生成的瞬間需要網(wǎng)格、材質(zhì)、骨骼動(dòng)畫、音效等一堆資源同時(shí)就位。對(duì)象和資源的關(guān)系就像住在毛坯房的人和不斷運(yùn)進(jìn)來(lái)的家具——沒(méi)人希望自己搬進(jìn)去當(dāng)天沙發(fā)還在路上。很多人會(huì)把Art和Resource搞混我先把這個(gè)最基礎(chǔ)的概念掰扯清楚。2.1 Asset和Resource真的是兩回事美術(shù)同學(xué)在Maya里導(dǎo)出的FBX、在Substance Painter里畫出來(lái)的TGA、在DAW里合成出來(lái)的WAV這些都是Asset——磁盤上的原始資產(chǎn)文件。它們的特點(diǎn)是體積巨大、格式五花八門、不適合運(yùn)行時(shí)直接讀取。運(yùn)行時(shí)引擎需要的是Resource——經(jīng)過(guò)導(dǎo)入管線加工后的目標(biāo)格式。拿紋理舉例美術(shù)給的是1024x1024的帶Alpha通道的TGA可能20MB大小。引擎導(dǎo)入時(shí)會(huì)把它轉(zhuǎn)成ASTC或BC7壓縮格式生成完整的mipmap鏈去掉不必要的通道最終體積可能只有2到4MB而且直接就是GPU喜歡讀取的布局。網(wǎng)格資源也一樣原始FBX是一堆節(jié)點(diǎn)層級(jí)加多邊形面導(dǎo)入后轉(zhuǎn)成Vulkan/D3D12緩存友好的頂點(diǎn)緩沖布局預(yù)計(jì)算好法線切線和包圍體。Asset和Resource的分離是讓游戲運(yùn)行時(shí)不卡頓的關(guān)鍵決策之一。否則你每次場(chǎng)景加載都要現(xiàn)去解析FBX、解壓TGA、再上傳GPU這個(gè)時(shí)間成本用戶根本等不起。資源管線本質(zhì)上是一條把時(shí)間花在編輯期而不是運(yùn)行期的流水線。2.2 CPU與GPU這兩座倉(cāng)庫(kù)成本各不同資源管理還要區(qū)分你管的是哪一頭的內(nèi)存。CPU側(cè)的資源是頂點(diǎn)數(shù)據(jù)、骨骼權(quán)重、碰撞體、音頻采樣解碼緩沖區(qū)GPU側(cè)的資源是紋理貼圖、Shader變體、頂點(diǎn)緩沖和索引緩沖。這兩側(cè)的預(yù)算完全不同CPU內(nèi)存可以靠操作系統(tǒng)換頁(yè)GPU顯存爆了就直接白屏或崩潰。所以我在項(xiàng)目里給資源加了一個(gè)budget屬性。每個(gè)資源類型在顯存或內(nèi)存里規(guī)定最大總?cè)萘砍龅牟糠肿吡魉筒呗浴獣簳r(shí)看不到的先卸載立刻要看的提前加載。分層到mipmap級(jí)別還有更細(xì)的操作LOD 0的貼圖只給近距離物件保留遠(yuǎn)處的只保留LOD 4之后的低精度層級(jí)每幀按相機(jī)視錐體去裁剪和調(diào)度。這個(gè)話題展開(kāi)聊能聊一天但你只需要記住一個(gè)原則資源管理的核心不是最大化利用硬件而是在用戶無(wú)感的情況下把每個(gè)資源在剛好的時(shí)間放到該在的地方。3. 實(shí)操一個(gè)可落地的資源管理與對(duì)象聯(lián)動(dòng)方案理論說(shuō)夠了講點(diǎn)我在自研引擎里實(shí)際落地的方案。這套設(shè)計(jì)談不上頂尖但經(jīng)過(guò)兩年線上項(xiàng)目的打磨穩(wěn)定性和排查效率都還可以。3.1 為什么資源句柄比裸指針安全游戲引擎里有一類經(jīng)典崩潰——資源被卸載了但某個(gè)組件還持有一個(gè)指向該資源的裸指針下次渲染時(shí)去讀那塊已經(jīng)釋放的內(nèi)存。輕則黑塊閃屏重則直接訪問(wèn)違例。解決方案是句柄Handle。句柄不是指針而是一個(gè)索引版本號(hào)的結(jié)構(gòu)通過(guò)它去資源表里間接查找真正的資源對(duì)象。當(dāng)資源被卸載時(shí)表項(xiàng)還在但版本號(hào)變掉了。任何持有舊句柄的代碼在解析時(shí)發(fā)現(xiàn)版本對(duì)不上就知道自己引用的資源已失效于是走兜底邏輯比如跳幀不畫、或者轉(zhuǎn)成一個(gè)錯(cuò)誤占位網(wǎng)格。這比裸指針那種祈禱它沒(méi)被復(fù)用的方式安全太多。一個(gè)實(shí)際的句柄結(jié)構(gòu)長(zhǎng)這樣struct ResourceHandle { uint32_t index; // 資源表中的槽位 uint32_t version; // 世代號(hào)每次復(fù)用時(shí)1 };獲取真實(shí)資源時(shí)做一次校驗(yàn)Resource* AcquireResource(const ResourceHandle handle) { auto slot resourceTable[handle.index]; if (slot.version ! handle.version || slot.state ! ResourceState::Ready) { return nullptr; // 資源已失效或不在位 } return slot.resource.get(); }這套方案唯一的代價(jià)是每次訪問(wèn)多一次間接跳轉(zhuǎn)和一次整數(shù)比較在C的優(yōu)化下幾乎可以忽略不計(jì)。換來(lái)的卻是整個(gè)系統(tǒng)層面的安全性。3.2 引用計(jì)數(shù)誰(shuí)需要它誰(shuí)就留它句柄解決了安全性但沒(méi)有解決什么時(shí)候資源才應(yīng)該卸載。如果一個(gè)角色正在使用某個(gè)貼圖掃場(chǎng)景時(shí)發(fā)現(xiàn)沒(méi)人加載它就把它釋放了那畫面上就出現(xiàn)一個(gè)大紫塊。所以資源生命周期必須靠引用計(jì)數(shù)來(lái)驅(qū)動(dòng)。引用計(jì)數(shù)的規(guī)則很簡(jiǎn)單誰(shuí)要使用某個(gè)資源誰(shuí)就讓計(jì)數(shù)加一用完必須減一。計(jì)數(shù)歸零時(shí)說(shuō)明沒(méi)有任何代碼需要使用這個(gè)資源可以安全釋放。但引用計(jì)數(shù)有個(gè)經(jīng)典死穴——循環(huán)引用。兩個(gè)資源互相引用對(duì)方A材料用到了B貼圖B的元數(shù)據(jù)又反指A材料。如果只靠強(qiáng)引用下去倆人的計(jì)數(shù)永遠(yuǎn)不會(huì)歸零資源就泄漏了。我處理這個(gè)問(wèn)題用的是經(jīng)典的強(qiáng)/弱雙引卸載檢查的時(shí)候只看強(qiáng)引用數(shù)量弱引用不阻止資源釋放。A對(duì)B是強(qiáng)引用B對(duì)A的引用標(biāo)成弱引用。B在被卸載的邊緣只要不因?yàn)槿跻枚槐W【蜁?huì)被釋放A在下一幀發(fā)現(xiàn)針對(duì)B的句柄失效了補(bǔ)一個(gè)默認(rèn)資源兜底。這相當(dāng)于承認(rèn)循環(huán)引用一定會(huì)存在并在架構(gòu)上讓它無(wú)害化。實(shí)際開(kāi)發(fā)里還需要額外小心一件事引用計(jì)數(shù)操作在多線程下必須原子化。加載線程和主線程可能同時(shí)引用同一個(gè)資源計(jì)數(shù)和--如果不做好同步多發(fā)幾次就直接錯(cuò)亂了。用std::atomic_ref或者平臺(tái)原子指令都是常規(guī)操作。3.3 異步加載與加載風(fēng)暴防護(hù)游戲運(yùn)行中最怕的是加載風(fēng)暴——玩家推開(kāi)一扇門門后是一個(gè)寬闊的開(kāi)放場(chǎng)景引擎一下子收到上百個(gè)資源加載請(qǐng)求IO線程滿負(fù)荷主線程等結(jié)果等到卡死。為了避免這種情況加載系統(tǒng)要做三件事優(yōu)先級(jí)隊(duì)列、預(yù)算控制、批量化提交。優(yōu)先級(jí)隊(duì)列好理解玩家面前5米內(nèi)的資源急加載30米外的慢加載。預(yù)算控制則是每幀限制發(fā)起多少個(gè)IO請(qǐng)求比如最多8個(gè)網(wǎng)格和16個(gè)紋理同時(shí)加載超出部分打包到下一幀再處理。批量化的意思是把不同資源的IO合并起來(lái)一次讀文件讀大塊避免頻繁隨機(jī)IO。異步加載的流程大概是這種狀態(tài)機(jī)enum class LoadTaskState { Pending, // 在隊(duì)列里排隊(duì) Loading, // IO線程正在讀文件 Processing,// 主線程做后處理如GPU上傳 Ready, // 可用 Failed // 加載失敗 };所有加載請(qǐng)求經(jīng)過(guò)一個(gè)任務(wù)管理器IO線程讀文件后放入處理隊(duì)列主線程每幀初批量處理把CPU側(cè)資源轉(zhuǎn)成GPU側(cè)資源再把句柄狀態(tài)置為Ready。預(yù)制體需要等到所有依賴資源Ready之后才能掛到場(chǎng)景里去。這也就是為什么異步加載接口都是給回調(diào)或者返回Future——因?yàn)橘Y源可用這個(gè)事件什么時(shí)候發(fā)生是不確定的代碼不能無(wú)限期阻塞主線程等它。3.4 資源卸載與關(guān)卡切換資源卸載比加載還要講究尤其是關(guān)卡切換的時(shí)候。無(wú)腦把所有資源清掉是最粗暴的做法但代價(jià)是UI字體、全局粒子系統(tǒng)的噪聲圖貼圖、音頻系統(tǒng)預(yù)加載的一堆常用音效這些跨關(guān)卡共享的東西被誤傷下一關(guān)開(kāi)頭又要全部加載回來(lái)黑屏?xí)r間肉眼可見(jiàn)地變長(zhǎng)。我用的方案是場(chǎng)景引用全局常駐區(qū)分離。每個(gè)場(chǎng)景都記錄自己引用了哪些資源場(chǎng)景卸載時(shí)只減少對(duì)應(yīng)資源的引用計(jì)數(shù)。全局常駐區(qū)里放的是UI字體、引擎默認(rèn)Shader、共享噪聲紋理、通用的圖集紋理這些在引擎初始化時(shí)加載一次整個(gè)生命周期內(nèi)引用計(jì)數(shù)永遠(yuǎn)不為零。再配合一個(gè)叫場(chǎng)景切換預(yù)加載的系統(tǒng)提前300毫秒到1秒就開(kāi)始加載下一關(guān)的頭部資源玩家在過(guò)場(chǎng)動(dòng)畫還沒(méi)走完的時(shí)候新場(chǎng)景的大部分資源已經(jīng)就位了。這套機(jī)制上線后我們項(xiàng)目切換關(guān)卡的硬暫停時(shí)間從原來(lái)的3到6秒降到1秒以內(nèi)而且是玩家無(wú)感的。這個(gè)優(yōu)化在手機(jī)端尤其值得做。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄這部分是我在實(shí)際項(xiàng)目里踩過(guò)坑之后總結(jié)出來(lái)的速查表希望能幫大家省點(diǎn)排查時(shí)間。4.1 場(chǎng)景切換卡成狗多半是加載策略不對(duì)癥狀切換場(chǎng)景時(shí)出現(xiàn)明顯卡頓從點(diǎn)擊到畫面出來(lái)花了十幾秒。排查思路先在Profiler里看時(shí)間消耗在哪個(gè)階段。是IO等待太久還是后處理階段上傳GPU太慢如果是IO等待多半是Assets沒(méi)有做批量打包或者是資源還是原始格式、解壓耗時(shí)嚴(yán)重。重新走一遍導(dǎo)入管線把資源換成運(yùn)行時(shí)格式。如果GPU上傳慢多半是加載請(qǐng)求全部趕在同一幀拋給了渲染線程觸發(fā)了上傳瓶頸。給上傳隊(duì)列加預(yù)算別讓主線程一口氣提交100個(gè)紋理上傳命令。這個(gè)問(wèn)題的根源在于加載調(diào)度策略而非硬件速度。資源系統(tǒng)沒(méi)有做優(yōu)先級(jí)和預(yù)算控制再好的硬盤也白搭。4.2 顯存超限導(dǎo)致黑屏/閃退癥狀游戲運(yùn)行一段時(shí)間后突然掉到黑屏Log一看是VK_ERROR_OUT_OF_DEVICE_MEMORY或者D3D12的DXGI_ERROR_DEVICE_REMOVED。大多數(shù)情況下這是紋理沒(méi)有及時(shí)卸載引起的。尤其是在開(kāi)放場(chǎng)景里自由探索時(shí)攝像機(jī)轉(zhuǎn)一圈新區(qū)域的貼圖全部加載進(jìn)來(lái)而離開(kāi)區(qū)域時(shí)它們的引用計(jì)數(shù)沒(méi)有減到零。排查方法在資源管理器里加一個(gè)實(shí)時(shí)監(jiān)控面板顯示每種資源的顯存占用、引用計(jì)數(shù)、最近一次訪問(wèn)時(shí)間。跑一遍地圖全走通的測(cè)試結(jié)束后看哪類資源的引用計(jì)數(shù)還在增長(zhǎng)基本就能定位到泄漏代碼的位置。常見(jiàn)泄漏點(diǎn)有物理引擎緩存了碰撞體網(wǎng)格、UI模塊持有已移除對(duì)象的材質(zhì)引用、特效播放完成但組件沒(méi)有正確釋放自己持有的資源。4.3 模型變灰或者變紫、貼圖閃爍癥狀角色或者某個(gè)物件的貼圖突然丟失顯示成純灰或紫白相間的默認(rèn)材質(zhì)。這大概率不是GPU壞了而是資源的引用失效后兜底邏輯觸發(fā)得太晚。需要做兩件事第一檢查資源是不是被提前卸載了。屬于生命周期管理不當(dāng)問(wèn)題通常是誰(shuí)把引用計(jì)數(shù)減多了。第二檢查句柄的版本號(hào)是否在錯(cuò)誤的時(shí)機(jī)遞增了。比如對(duì)象池復(fù)用對(duì)象時(shí)舊組件的句柄沒(méi)有隨對(duì)象失活而失效等對(duì)象復(fù)用后舊句柄版本沒(méi)對(duì)上也借不到資源導(dǎo)致的。我推薦在Debug構(gòu)建里加一個(gè)資源訪問(wèn)審計(jì)模式每次AcquireResource拿不到真實(shí)資源時(shí)輸出一個(gè)警告日志帶上調(diào)用棧。收集一輪測(cè)試的日志你就能看清楚是誰(shuí)在錯(cuò)誤地訪問(wèn)資源。這比對(duì)著代碼一行行檢查快得多。4.4 同一個(gè)資源被加載了好幾份癥狀關(guān)卡內(nèi)存占用比預(yù)估高出很多排查下來(lái)發(fā)現(xiàn)同一個(gè)模型或材質(zhì)在內(nèi)存里有多個(gè)實(shí)例。這是資源去重Dedup沒(méi)做好的典型問(wèn)題。很多引擎資源加載入口是按路徑走的但如果路徑字符串大小寫不統(tǒng)一、或者加載子系統(tǒng)重入導(dǎo)致路徑規(guī)范化不一致同一個(gè)文件就會(huì)被加載兩次。解決辦法是在資源管理器內(nèi)部做統(tǒng)一的路徑規(guī)范化所有加載線程都用同一條路徑經(jīng)過(guò)hash映射到資源表。加載前先查表中了就返回現(xiàn)有資源不中就創(chuàng)建新的。還有一個(gè)細(xì)節(jié)打包之后的bundle里資源路徑不帶原始文件目錄這時(shí)候要用GUID而不是路徑作為key避免不同目錄下的同名文件互相撞車。我在項(xiàng)目里給資源表加了一列GUID保證任何形式的重入都找不到第二份。實(shí)測(cè)內(nèi)存占用降低了將近20%主要是貼圖和網(wǎng)格這類大頭有了真正的單例保證。5. 寫在最后的一些經(jīng)驗(yàn)?zāi)銜?huì)發(fā)現(xiàn)游戲?qū)ο蠛唾Y源管理這兩個(gè)主題在架構(gòu)層其實(shí)是一體兩面的——游戲?qū)ο竺枋龅氖钦l(shuí)在世界上存在資源管理描述的是他們用什么來(lái)表現(xiàn)自己。兩者在代碼上嚴(yán)格解耦但在數(shù)據(jù)流上緊密聯(lián)動(dòng)這正是引擎架構(gòu)最有意思的地方。另外提一個(gè)我最近在折騰的方向給資源管理系統(tǒng)加上引用追蹤的可視化工具。把每個(gè)資源的引用鏈用有向圖的方式實(shí)時(shí)畫出來(lái)疊加在場(chǎng)景編輯器上。哪些資源被誰(shuí)在用哪些資源是雖然沒(méi)人用但還活著的僵尸一眼就能看出來(lái)。這個(gè)工具的雛形已經(jīng)跑通了等穩(wěn)定了再寫一篇分享。如果你正在設(shè)計(jì)自己的引擎或者打算給現(xiàn)有引擎做對(duì)象和資源的重構(gòu)我建議你一開(kāi)始就把句柄機(jī)制、引用計(jì)數(shù)、生命周期狀態(tài)機(jī)和異步加載這四件事定下來(lái)。它們是整個(gè)系統(tǒng)穩(wěn)定運(yùn)行的地基后面再填功能都是在上面加磚。中途返工的代價(jià)可比一開(kāi)始多寫幾千行代碼要高得多。