UdonSharpで別コンポーネントのメンバ変数アクセス時のGCアロケート削減
経緯
UdonSharpで大富豪ワールドを作っていた時に以下の記事を見て、なんとかGCアロケートを削減しようと試行錯誤した際のアイデア記事です。
上記記事の U#は別コンポーネントのメソッド呼び出しがコストの箇所に対してです。
この中の、別コンポーネントのメンバ変数アクセス時のGCアロケート削減に絞って記載します。
結論
配列経由でアクセスするとGCアロケートを削減できそうです。(デメリットあります。)
一言で書いてもよく分からないので以下コードイメージ。
//呼び出し先: 配列
public int[] valArray = new int[1];
[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#コード (メンバ変数直アクセス)
[SerializeField]
private DstObj dstObj = null;
void Start(){}
private void Update()
{
for (int i = 0; i < 1000; i++) {
//プリミティブ型でアクセス
int tmp = dstObj.val;
}
}
- 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#コード (配列経由アクセス)
[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 (配列経由アクセス)
_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アロケート調査しました。
Discussion