il2cpp_IL2CPP优化:避免装箱

本文介绍了IL2CPP如何避免不必要的装箱操作,以提高性能。C#的装箱会带来堆分配、垃圾回收的额外开销,IL2CPP通过针对特定类型生成优化代码,消除对于值类型不必要的装箱,从而提升代码执行效率。
摘要由CSDN通过智能技术生成

il2cpp

In this final episode of our IL2CPP micro-optimization miniseries, we’ll explore the high cost of something called “boxing”, and we’ll see how IL2CPP can avoid it when it is done unnecessarily.

在我们的IL2CPP微型优化迷你系列的最后一集中,我们将探讨被称为“装箱”的高成本,并且我们将了解IL2CPP如何避免不必要的操作。

堆分配很慢 (Heap allocations are slow)

Like many programming languages, C# allows the memory for objects to be allocated on the stack (a small, “fast”, scope-specific, block of memory) or the heap (a large, “slow”, global block of memory). Usually allocating space for an object on the heap is much more expensive than allocating space on the stack. It also involves tracking that allocated memory in the garbage collector, which has an additional cost. So we try to avoid heap allocations where possible.

像许多编程语言一样,C#允许将对象的内存分配到堆栈(较小的,“快速的”,特定于作用域的内存块)或堆(较大的,“慢的”,全局内存块)上。 通常,在堆上为对象分配空间比在堆栈上分配空间要昂贵得多。 它还涉及在垃圾回收器中跟踪已分配的内存,这会产生额外的费用。 因此,我们尝试在可能的情况下避免堆分配。

C# lets us do this by separating types into value types (which can be allocated on the stack), and reference types (which must be allocated on the heap). Types like int and float are value types, string and object are reference types. User-defined value types use the struct keyword. User-defined reference types use the class keyword. Note that a value type can never hold a the value null. In C#, the null value can only be assigned to reference types. Keep this distinction in mind as we continue.

C#通过将类型分为 值类型 (可以在堆栈上分配)和 引用类型 (必须在堆上分配)来做到这一点。 像 int 和 float 这样的类型是值类型, string 和 object 是引用类型。 用户定义的值类型使用 struct 关键字。 用户定义的引用类型使用 class 关键字。 请注意,值类型永远不能保留值null。 在C#中,只能将空值分配给引用类型。 在继续操作时,请记住这一区别。

Being good performance citizens, we try to avoid heap allocations unless they are necessary. But sometimes we need to convert a value type on the stack into a reference type on the heap. This process is called boxing. Boxing:

作为良好性能的公民,除非有必要,否则我们将避免堆分配。 但是有时我们需要将堆栈上的值类型转换为堆上的引用类型。 此过程称为 装箱 。 拳击:

  1. Allocates space on the heap

    在堆上分配空间

  2. Informs the garbage collector about the new object

    通知垃圾收集器有关新对象的信息

  3. Copies the data from the value type object into the new reference type object

    将数据从值类型对象复制到新的引用类型对象中

Ugh, let’s add boxing to our list of things to avoid!

gh,让我们将拳击添加到要避免的事情清单中!

那个讨厌的编译器 (That pesky compiler)

Suppose we are happily writing code, avoiding unnecessary heap allocations and boxing. Maybe we have some trees for our world, and each has a size which scales with its age:

假设我们愉快地编写代码,避免不必要的堆分配和装箱。 也许我们的世界有几棵树,每棵树的大小都随年龄而变化:

1

2
3
4
5
6
7
8
9
10
11
12
13
14
interface HasSize {
   int CalculateSize();
}
struct Tree : HasSize {
   private int years;
   public Tree(int age) {
       years = age;
   }
   public int CalculateSize() {
       return years*3;
   }
}

1

2
3
4
5
6
7
8
9
10
11
12
13
14
interface HasSize {
   int CalculateSize ( ) ;
}
struct Tree : HasSize {
   private int years ;
   public Tree ( int age ) {
       years = age ;
   }
   public int CalculateSize ( ) {
       return years* 3 ;
   }
}

Elsewhere in our code, we have this convenient method to sum up the size of many things (including possibly Tree objects):

在代码的其他地方,我们有这种方便的方法来汇总许多事物(包括 Tree 对象)的大小:

1

2
3
4
5
6
7
8
public static int TotalSize<T>(params T[] things) where T : HasSize
{
   var total = 0;
   for (var i = 0; i < things.Length; ++i)
       if (things[i] != null)
           total += things[i].CalculateSize();
   return total;
}

1

2
3
4
5
6
7
8
public static int TotalSize < T > ( params T [ ] things ) where T : HasSize
{
   var total = 0 ;
   for ( var i = 0 ; i < things . Length ; ++ i )
       if ( things [ i ] != null )
           total += things [ i ] . CalculateSize ( ) ;
   return total ;
}

This looks safe enough, but let’s peer into a little bit of the Intermediate Language (IL) code that the C# compiler generates:

这看起来足够安全,但让我们看一下C#编译器生成的一些中间语言(IL)代码:

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// This is the start of the for loop
// Load the array
IL_0009: ldarg.0
// Load the current index
IL_000a: ldloc.1
// Load element at the current index
IL_000b: ldelem.any !!T
// What is this box call doing in here?!?
// (Hint: see the null check in the C# code)
IL_0010: box !!T
IL_0015: brfalse IL_002f
// Set up the arguments for the method and it call
IL_001a: ldloc.0
IL_001b: ldarg.0
IL_001c: ldloc.1
IL_001d: ldelema !!T
IL_0022: constrained. !!T
IL_0028: callvirt instance int32 Unity.IL2CPP.IntegrationTests.Tests.ValueTypeTests.ValueTypeTests/
                                   IHasSize::CalculateSize()
IL_002f: // Do the next loop iteration...

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// This is the start of the for loop
// Load the array
IL_0009 : ldarg . 0
// Load the current index
IL_000a : ldloc . 1
// Load element at the current index
IL_000b : ldelem . any ! ! T
// What is this box call doing in here?!?
// (Hint: see the null check in the C# code)
IL_0010 : box ! ! T
IL_0015 : brfalse IL_002f
// Set up the arguments for the method and it call
IL_001a : ldloc . 0
IL_001b : ldarg . 0
IL_001c : ldloc . 1
IL_001d : ldelema ! ! T
IL_0022 : constrained . ! ! T
IL_0028 : callvirt instance int32 Unity . IL2CPP . IntegrationTests . Tests . ValueTypeTests . ValueTypeTests /
                                   IHasSize :: CalculateSize ( )
IL_002f : // Do the next loop iteration...

The C# compiler has implemented the if (things[i] != null) check using boxing! If the type T is already a reference type, then the box opcode is pretty cheap – it just returns the existing pointer to the array element. But if type T is a value type (like our Tree type), then that box opcode is very costly. Of course, value types can never be null, so why do we need to implement the check in the first place? And what if we need to compute the size of one hundred Tree objects, or maybe one thousand Tree objects? That unnecessary boxing will quickly become very important.

C#编译器已 使用装箱 实现了 if(things [i]!= null) 检查! 如果类型 T 已经是引用类型,则 框 操作码非常便宜-它仅返回指向数组元素的现有指针。 但是,如果类型 T 是一个值类型(例如我们的 Tree 类型),那么该 框 操作码将 非常 昂贵。 当然,值类型永远不能为 null ,那么为什么我们首先需要实现检查? 如果我们需要计算一百个 Tree 对象或一千个 Tree 对象 的大小,该 怎么办? 不必要的拳击将很快变得 非常 重要。

最快的代码是您不执行的任何东西 (The fastest code is anything you don’t execute)

The C# compiler needs to provide a general implementation that works for any possible type T, so it is stuck with this slower code. But a compiler like IL2CPP can be a bit more aggressive when it generates code that will be executed and when it doesn’t generate the code that won’t!

C#编译器需要提供适用于任何可能的类型 T 的通用实现 ,因此它会卡在此较慢的代码中。 但是,像IL2CPP这样的编译器在生成将要执行的代码时以及在不生成将不会执行的代码时可能更具攻击性!

IL2CPP will create an implementation of The TotalSize<T> method specifically for the case where T is a Tree. the IL code above looks like this in generated C++ code:

IL2CPP将 专门针对 T 是 Tree 的情况 创建 TotalSize <T> 方法 的实现 。 上面的IL代码在生成的C ++代码中看起来像这样:

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
IL_0009:
// Load the array
TreeU5BU5D_t4162282477* L_0 = ___things0;
// Load the current index
int32_t L_1 = V_1;
NullCheck(L_0);
IL2CPP_ARRAY_BOUNDS_CHECK(L_0, L_1);
int32_t L_2 = L_1;
// Load the element at the current index
Tree_t1533456772  L_3 = (L_0)->GetAt(static_cast<il2cpp_array_size_t>(L_2));
// Look Ma, no box and no branch!
// Set up the arguments for the method and it call
int32_t L_4 = V_0;
TreeU5BU5D_t4162282477* L_5 = ___things0;
int32_t L_6 = V_1;
NullCheck(L_5);
IL2CPP_ARRAY_BOUNDS_CHECK(L_5, L_6);
int32_t L_7 = Tree_CalculateSize_m1657788316((Tree_t1533456772 *)(
                (L_5)->GetAddressAt(static_cast<il2cpp_array_size_t>(L_6))), /*hidden argument*/NULL);
// Do the next loop iteration...

1

2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
IL_0009 :
// Load the array
TreeU5BU5D_t4162282477* L_0 = ___things0 ;
// Load the current index
int32_t L_1 = V_1 ;
NullCheck ( L_0 ) ;
IL2CPP_ARRAY_BOUNDS_CHECK ( L_0 , L_1 ) ;
int32_t L_2 = L_1 ;
// Load the element at the current index
Tree _ t1533456772  L_3 = ( L_0 ) -> GetAt ( static_cast < il2cpp_array_size_t > ( L_2 ) ) ;
// Look Ma, no box and no branch!
// Set up the arguments for the method and it call
int32_t L_4 = V_0 ;
TreeU5BU5D_t4162282477* L_5 = ___things0 ;
int32_t L_6 = V_1 ;
NullCheck ( L_5 ) ;
IL2CPP_ARRAY_BOUNDS_CHECK ( L_5 , L_6 ) ;
int32_t L_7 = Tree_CalculateSize_m1657788316 ( ( Tree_t1533456772 * ) (
                 ( L_5 ) -> GetAddressAt ( static_cast < il2cpp_array_size_t > ( L_6 ) ) ) , /*hidden argument*/ NULL ) ;
// Do the next loop iteration...

IL2CPP recognized that the box opcode is unnecessary for a value type, because we can prove ahead of time that a value type object will never be null. In a tight loop, this removal of an unnecessary allocation and copy of data can have a significant positive impact on performance.

IL2CPP认识到 对于值类型而言, 框 操作码不是必需的,因为我们可以提前证明值类型对象永远不会为 null 。 在一个紧密的循环中,这种不必要的分配和数据副本的删除会对性能产生重大的积极影响。

结语 (Wrapping up)

As with the other micro-optimizations discussed in this series, this one is a common optimization for .NET code generators. All of the scripting backends used by Unity currently perform this optimization for you, so you can get back to writing your code.

与本系列中讨论的其他微优化一样,这是.NET代码生成器的常见优化。 当前,Unity使用的所有脚本后端均会为您执行此优化,因此您可以重新开始编写代码。

We hope you have enjoyed this miniseries about micro-optimizations. As we continue to improve the code generators and runtimes used by Unity, we’ll offer more insight into the micro-optimizations that go on behind the scenes.

我们希望您喜欢这个关于微优化的迷你系列。 随着我们继续改进Unity使用的代码生成器和运行时,我们将提供更多有关幕后微优化的信息。

翻译自: https://blogs.unity3d.com/2016/08/11/il2cpp-optimizations-avoid-boxing/

il2cpp

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值