🐼

UdonSharpで別コンポーネントのメンバ変数アクセス時のGCアロケート削減

2022/04/16に公開

経緯

UdonSharpで大富豪ワールドを作っていた時に以下の記事を見て、なんとかGCアロケートを削減しようと試行錯誤した際のアイデア記事です。

https://qiita.com/toRisouP/items/16bd06aa303a1bb1a747#uは別コンポーネントのメソッド呼び出しがコスト

上記記事の U#は別コンポーネントのメソッド呼び出しがコストの箇所に対してです。
この中の、別コンポーネントのメンバ変数アクセス時のGCアロケート削減に絞って記載します。

結論

配列経由でアクセスするとGCアロケートを削減できそうです。(デメリットあります。)
一言で書いてもよく分からないので以下コードイメージ。

Callee.cs(呼び出し先)
//呼び出し先: 配列
public int[] valArray = new int[1];
Caller.cs(呼び出し元)
[SerializeField]
private Callee _callee; //呼び出し先
private int[] _callee_valArray = null; //呼び出し先変数の参照
void Start(){
    //呼び出し先配列の参照取得
    _callee_valArray = _callee.valArray;
}
void Update(){
    //配列経由でアクセス
    int tmp = _callee_valArray[0]; //読み
    _callee_valArray[0] = tmp + 1; //書き
}

セッター/ゲッター経由でも、プロパティ経由でもVRCUdonCommonInterfacesIUdonEventReceiverのSendCustomEvent/GetProgramVariable/SetProgramVariableを呼んでしまうためGCアロケートが発生します。
変数直呼びでもGetProgramVariable/SetProgramVariableは発生します。
なんとなく配列経由であれば大丈夫なのではないかと考え検証しました。

  • アクセスイメージ(int変数)

検証した結果、配列経由のアクセスであればSystemInt32ArrayのGet__SystemInt32/Set__SystemInt32でありGCアロケートが削減されるようです。

毎ループで他コンポーネントの状態を確認する場合などに効果を発揮すると思います。
ただしデメリットがあります。

検証

2コンポーネント間の変数読み込みに対して、メンバ変数直アクセスと配列経由アクセスの場合でProfilerによる比較結果を記載します。
どちらも一回のUpdateの中で1000回の変数読み込みをしています。
ClientSimで動かしています。

  • 環境
- バージョン
Unity 2019.4.31f1
VRCSDK VRCSDK3-WORLD-2022.02.16.19.13_Public
UdonSharp UdonSharp_v0.20.3
ClientSim #9205b40a

メンバ変数直アクセスの実行コード

  • C#コード (メンバ変数直アクセス)
呼び出し元(C#)
[SerializeField]
private DstObj dstObj = null;

void Start(){}

private void Update()
{
    for (int i = 0; i < 1000; i++) {
        //プリミティブ型でアクセス
        int tmp = dstObj.val;
    }
}
  • UdonAssembly (メンバ変数直アクセス)
呼び出し元(UdonAssembly)
_update:
    
    PUSH, __0_const_intnl_SystemUInt32
    
     #      {
    
     #          for (int i = 0; i < 1000; i++) {
    PUSH, __0_const_intnl_SystemInt32
    PUSH, __0_i_Int32
    COPY
    PUSH, __0_i_Int32
    PUSH, __1_const_intnl_SystemInt32
    PUSH, __0_intnl_SystemBoolean
    EXTERN, "SystemInt32.__op_LessThan__SystemInt32_SystemInt32__SystemBoolean"
    PUSH, __0_intnl_SystemBoolean
    JUMP_IF_FALSE, 0x00000124
    
     #              int tmp = dstObj.val;
    PUSH, dstObj
    PUSH, __1_const_intnl_SystemString
    PUSH, __1_intnl_SystemObject
    EXTERN, "VRCUdonCommonInterfacesIUdonEventReceiver.__GetProgramVariable__SystemString__SystemObject"
    PUSH, __1_intnl_SystemObject
    PUSH, __0_tmp_Int32
    EXTERN, "SystemConvert.__ToInt32__SystemObject__SystemInt32"
    PUSH, __0_i_Int32
    PUSH, __0_intnl_SystemInt32
    COPY
    PUSH, __0_intnl_SystemInt32
    PUSH, __2_const_intnl_SystemInt32
    PUSH, __0_i_Int32
    EXTERN, "SystemInt32.__op_Addition__SystemInt32_SystemInt32__SystemInt32"
    JUMP, 0x00000080
    PUSH, __0_intnl_returnTarget_UInt32 # Function epilogue
    COPY
    JUMP_INDIRECT, __0_intnl_returnTarget_UInt32
    

配列経由アクセスの実行コード

  • C#コード (配列経由アクセス)
呼び出し元(C#)
[SerializeField]
private DstObj dstObj = null;
private int[] _dstObj_valArray = null;

void Start()
{
    _dstObj_valArray = dstObj.valArr;
}

private void Update()
{
    for (int i = 0; i < 1000; i++) {
        //配列でアクセス
        int tmp = _dstObj_valArray[0];
    }
}
  • UdonAssembly (配列経由アクセス)
呼び出し元(UdonAssembly)
    _update:
        
        PUSH, __0_const_intnl_SystemUInt32
        
         #      {
        
         #          for (int i = 0; i < 1000; i++) {
        PUSH, __0_const_intnl_SystemInt32
        PUSH, __0_i_Int32
        COPY
        PUSH, __0_i_Int32
        PUSH, __1_const_intnl_SystemInt32
        PUSH, __0_intnl_SystemBoolean
        EXTERN, "SystemInt32.__op_LessThan__SystemInt32_SystemInt32__SystemBoolean"
        PUSH, __0_intnl_SystemBoolean
        JUMP_IF_FALSE, 0x0000010C
        
         #              int tmp = _dstObj_valArray[0];
        PUSH, _dstObj_valArray
        PUSH, __0_const_intnl_SystemInt32
        PUSH, __0_tmp_Int32
        EXTERN, "SystemInt32Array.__Get__SystemInt32__SystemInt32"
        PUSH, __0_i_Int32
        PUSH, __0_intnl_SystemInt32
        COPY
        PUSH, __0_intnl_SystemInt32
        PUSH, __2_const_intnl_SystemInt32
        PUSH, __0_i_Int32
        EXTERN, "SystemInt32.__op_Addition__SystemInt32_SystemInt32__SystemInt32"
        JUMP, 0x00000080
        PUSH, __0_intnl_returnTarget_UInt32 # Function epilogue
        COPY
        JUMP_INDIRECT, __0_intnl_returnTarget_UInt32

検証結果

ProfilerのHierarchyの結果比較です。
GC Allocも処理時間も減っています。

  • メンバ変数直アクセス
    GC Alloc : 19.5KB
    UdonBehaviour.ManagedUpdate()のTime ms : 55.3ms

  • 配列経由アクセス
    GC Alloc : 0KB
    UdonBehaviour.ManagedUpdate()のTime ms : 30.85ms

デメリット

開発がとてもめんどくさくなります。
セッター/ゲッター/プロパティを使う場合に比べ、他コンポーネントの配列の参照を持つことになりコードが煩雑になり、初期化の手間も増えます。

備考

その他処理のGCアロケート調査しました。
https://zenn.dev/panda_nakami/articles/20220626-vrchat-udon-alloc-research

Discussion