(Unity) 為被 Lua 隔斷的 C# 實現添加 Profiler 支援

來源:互聯網
上載者:User

問題描述

Unity 項目在實踐中往往選擇使用 Lua 作為更上層的邏輯指令碼。這一方面是由於 Unity 本身對熱更不是很友好,用 Lua 熱更靈活得多,另一方面也是簡化與伺服器共用代碼和資料。目前多種不同的 Unity + Lua 整合方案中,實踐中採用比較多的是龐巍偉同學的 slua 方案。

使用 Lua 的團隊,往往傾向於“較重的”整合,也就是暴露相當大規模的引擎介面給 Lua,這樣邏輯上才能有足夠的自由度對遊戲和引擎做全面的控制。當 C#/Lua 之間的互操作介面迅速增長到成千上萬的數量時,一個重要的問題就會浮現出來:C# 和 Lua 的互動層對引擎是封閉的,很多引擎內建的工具,沒有辦法跨越宿主 (C#) 和指令碼 (Lua) 的邊界。這些受到影響的機制裡,最重要的就是 Unity Profiler。

Unity Profiler 是 Unity 提供的一個有力的效能分析工具 ,能夠在最佳化階段有效地協助定位瓶頸,有時也容易藉機發現一些潛藏的 bug。而當我們定位到某個 Lua 函數有較大的開銷(CPU 或記憶體 GC Alloc)時,由於跨語言邊界的影響被阻攔,就難以進一步觀察更多的細節。

“正常的”做法

由於 Profiler 的 API 介面也一併暴露給了指令碼,正常的做法是:根據 C# 這邊的有問題的調用,翻到對應的 Lua 代碼,把相關的指令碼邏輯讀一遍,為那些潛在的開銷大的邏輯添加對應的效能剖析採樣 Profiler.BeginSample()/EndSample(),來定位問題程式碼片段落,然後再翻回對應 C# 函數,再在裡面加上測試代碼印證我們的想法。

實際上我們知道,大多數情況下(如果不算 bug),與引擎部分相比,邏輯指令碼的 CPU 開銷是相對比較低的(邏輯代碼裡以 if 判斷居多,遇到需要迴圈的情況都非常少,一般用不到啥非常複雜的運算——或者說在設計得當的情況下,複雜的運算都會交給底層去整塊整塊地做),而容易造成困擾的託管記憶體配置導致 GC 卡頓的記憶體問題,也是完全由指令碼調回來的 C# 代碼造成的。

這樣分析下來,往往繞一圈又回到 C# 裡,中間付出了大量無謂的在指令碼裡兜圈子的時間不說,被逼著讀和分析重複性高的指令碼邏輯代碼,也大大增加了精力和腦力的負擔。

AOP

既然知道了反正總是要從 Lua 回到 C# 的,那麼有沒有什麼簡單的辦法,一勞永逸地為所有暴露給 Lua 的 C# 介面加上效能剖析採樣呢?

如果能做到這一點,我們就可以無視中間指令碼層 (Lua) 的幹擾,在 C# 環境內解決所有問題。

俺的目光很自然地投向了 AOP (Aspect Oriented Programming),這種技術能幫我們在不用修改目標函數代碼的情況下,加入我們想執行的代碼(就像 Python 的 decorator 那樣)。

python-deco

經過一番研究,我成功地得出以下這條結論:

現有的一些針對 C# 的 AOP 方法,在 Unity 的 mono 下,基本都跪了~~

還能不能讓俺過一個快樂的兒童節了~~

在這些嘗試裡,最接近成功的是:使用 lambda expression 包一層,添加相關代碼後,再註冊給 slua,然而,slua 需要為註冊進來的函數添加下面的 attribute:

    [MonoPInvokeCallbackAttribute (typeof (xxx))]
而 C# 不支援為 lambda expression 添加 attribute,所以 &_& ……

利用 slua 代碼產生的簡明做法

發現難以通過 C# 本身的語言機制解決問題之後,我把目光投向了 slua:既然所有的綁定代碼是 slua 產生的,那麼不如直接修改產生代碼,把採樣代碼產生到介面的綁定函數裡~

找到普通函數介面的產生位置

void WriteFunctionImpl( StreamWriter file, MethodInfo m, Type t, BindingFlags bf)
{
    ...
}
在一頭一尾添加了對應的產生代碼(BeginSample() 的參數可以直接用 MethodInfo.Name 得到正確的函數名 ),運行 slua 的 Make 產生一下,得到下面的結果(單個函數):

[MonoPInvokeCallbackAttribute(typeof(xxx))]
static public int xxx(IntPtr l) {
    try {
        Profiler. BeginSample( "xxx");

        ...

        return 1;
    }
    catch( Exception e) {
        return error( l, e);
    }
    finally {
        Profiler. EndSample();
    }
}
EndSample() 在 finally 內,保證每個出口都能正確配對。

粒度控制

接下來更進一步,我們希望有某種粒度的控制能力,只為某個關心的類產生,甚至只為該類內關心的函數產生。回到前面的函數產生所在的方法,可以看到簽名:

void WriteFunctionImpl( StreamWriter file, MethodInfo m, Type t, BindingFlags bf);
其中第二個參數可以用來篩選我們關心的函數(可以跟 m.Name 比較來過濾字串),而第三個參數 Type t 可以用來篩選對應的類(通過 if ( t == typeof(TargetClass))),這樣就可以只在我們需要的時候,為特定的類和函數產生了。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.