簡體   English   中英

為什么拆箱比拳擊快100倍

[英]Why unboxing is 100 time faster than boxing

為什么拳擊和拆箱操作之間的速度變化如此之大? 有10倍的差異。 我們什么時候應該關心這個? 上周Azure支持告訴我們,我們的應用程序的堆內存中存在問題。 我很想知道它是否與裝箱拆箱問題有關。

using System;
using System.Diagnostics;

namespace ConsoleBoxing
{
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Program started");
        var elapsed = Boxing();
        Unboxing(elapsed);
        Console.WriteLine("Program ended");
        Console.Read();
    }

    private static void Unboxing(double boxingtime)
    {
        Stopwatch s = new Stopwatch();
        s.Start();
        for (int i = 0; i < 1000000; i++)
        {
            int a = 33;//DATA GOES TO STACK
            object b = a;//HEAP IS REFERENCED
            int c = (int)b;//unboxing only hEre ....HEAP GOES TO STACK
        }
        s.Stop();

        var UnBoxing =  s.Elapsed.TotalMilliseconds- boxingtime;
        Console.WriteLine("UnBoxing time : " + UnBoxing);
    }

    private static double Boxing()
    {
        Stopwatch s = new Stopwatch();
        s.Start();
        for (int i = 0; i < 1000000; i++)
        {
            int a = 33;
            object b = a;
        }
        s.Stop();
        var elapsed = s.Elapsed.TotalMilliseconds;
        Console.WriteLine("Boxing time : " + elapsed);
        return elapsed;
    }
}
}

可以將拆箱視為從盒裝​​對象到寄存器的單個內存加載指令。 可能有一些周圍的地址計算和轉換驗證邏輯。 盒裝對象就像一個帶有一個盒裝類型字段的類。 這些操作有多貴? 不是很特別,因為基准測試中的L1緩存命中率約為100%。

拳擊涉及分配一個新的對象和GC以后。 在您的代碼中,GC可能會在99%的情況下觸發分配。

這表示你的基准測試無效,因為循環沒有副作用。 目前的JIT可能無法優化它們。 以某種方式讓循環計算結果並將其GC.KeepAliveGC.KeepAlive以使結果顯示為使用。 此外,您可能正在運行調試模式。

雖然人們已經提供了很好的解釋,為什么拆箱比拳擊更快。 我想更多地談談用於測試性能差異的方法。

你從你發布的代碼中得到了你的結果(速度差異是10倍)嗎? 如果我在發布模式下運行該程序,這是輸出:

Program started
Boxing time : 0.2741
UnBoxing time : 4.5847
Program ended

每當我進行微觀性能基准測試時,我傾向於進一步驗證我確實在比較我想要比較的操作。 編譯器可以對您的代碼進行優化。 在ILDASM中打開可執行文件:

這是拆箱的IL :(我只包括最重要的部分)

IL_0000:  newobj     instance void [System]System.Diagnostics.Stopwatch::.ctor()
IL_0005:  stloc.0
IL_0006:  ldloc.0 
IL_0007:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Start()
IL_000c:  ldc.i4.0
IL_000d:  stloc.1
IL_000e:  br.s       IL_0025
IL_0010:  ldc.i4.s   33
IL_0012:  stloc.2
IL_0013:  ldloc.2
IL_0014:  box        [mscorlib]System.Int32    //Here is the boxing
IL_0019:  stloc.3
IL_001a:  ldloc.3
IL_001b:  unbox.any  [mscorlib]System.Int32    //Here is the unboxing
IL_0020:  pop
IL_0021:  ldloc.1
IL_0022:  ldc.i4.1
IL_0023:  add
IL_0024:  stloc.1
IL_0025:  ldloc.1
IL_0026:  ldc.i4     0xf4240
IL_002b:  blt.s      IL_0010
IL_002d:  ldloc.0
IL_002e:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Stop()

這是拳擊的代碼:

IL_0000:  newobj     instance void [System]System.Diagnostics.Stopwatch::.ctor()
IL_0005:  stloc.0
IL_0006:  ldloc.0
IL_0007:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Start()
IL_000c:  ldc.i4.0
IL_000d:  stloc.1
IL_000e:  br.s       IL_0017
IL_0010:  ldc.i4.s   33
IL_0012:  stloc.2
IL_0013:  ldloc.1
IL_0014:  ldc.i4.1
IL_0015:  add
IL_0016:  stloc.1
IL_0017:  ldloc.1
IL_0018:  ldc.i4     0xf4240
IL_001d:  blt.s      IL_0010
IL_001f:  ldloc.0
IL_0020:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Stop()

在拳擊方法中根本沒有拳擊指令 它已被編譯器完全刪除。 Boxing方法除了迭代空循環外什么都不做。 因此,在UnBoxing中測量的時間將成為裝箱和拆箱的總時間。

微基准測試非常容易受到編譯器技巧的影響。 我建議你也看看你的IL。 如果您使用不同的編譯器,可能會有所不同。

我稍微修改了你的測試代碼:

拳擊方法:

private static object Boxing()
{
    Stopwatch s = new Stopwatch();

    int unboxed = 33;
    object boxed = null;

    s.Start();

    for (int i = 0; i < 1000000; i++)
    {
        boxed = unboxed;
    }

    s.Stop();

    var elapsed = s.Elapsed.TotalMilliseconds;
    Console.WriteLine("Boxing time : " + elapsed);

    return boxed;
}

和拆箱方法:

private static int Unboxing()
{
    Stopwatch s = new Stopwatch();

    object boxed = 33;
    int unboxed = 0;

    s.Start();

    for (int i = 0; i < 1000000; i++)
    {
        unboxed = (int)boxed;
    }

    s.Stop();

    var time = s.Elapsed.TotalMilliseconds;
    Console.WriteLine("UnBoxing time : " + time);

    return unboxed;
}

這樣他們就可以翻譯成類似的IL:

對於拳擊方法:

IL_000c:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Start()
IL_0011:  ldc.i4.0
IL_0012:  stloc.3
IL_0013:  br.s       IL_0020
IL_0015:  ldloc.1
IL_0016:  box        [mscorlib]System.Int32  //Here is the boxing
IL_001b:  stloc.2
IL_001c:  ldloc.3
IL_001d:  ldc.i4.1
IL_001e:  add
IL_001f:  stloc.3
IL_0020:  ldloc.3
IL_0021:  ldc.i4     0xf4240
IL_0026:  blt.s      IL_0015
IL_0028:  ldloc.0
IL_0029:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Stop()

對於取消裝箱:

IL_0011:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Start()
IL_0016:  ldc.i4.0
IL_0017:  stloc.3
IL_0018:  br.s       IL_0025
IL_001a:  ldloc.1
IL_001b:  unbox.any  [mscorlib]System.Int32  //Here is the UnBoxng
IL_0020:  stloc.2
IL_0021:  ldloc.3
IL_0022:  ldc.i4.1
IL_0023:  add
IL_0024:  stloc.3
IL_0025:  ldloc.3
IL_0026:  ldc.i4     0xf4240
IL_002b:  blt.s      IL_001a
IL_002d:  ldloc.0
IL_002e:  callvirt   instance void [System]System.Diagnostics.Stopwatch::Stop()

運行幾個循環以刪除冷啟動效果:

static void Main(string[] args)
{
    Console.WriteLine("Program started");
    for (int i = 0; i < 10; i++)
    {
        Boxing();
        Unboxing();
    }
    Console.WriteLine("Program ended");
    Console.Read();
}

這是輸出:

Program started
Boxing time : 3.4814
UnBoxing time : 0.1712
Boxing time : 2.6294
...
Boxing time : 2.4842
UnBoxing time : 0.1712
Program ended

這是否證明拆箱比拳擊快10倍 讓我們用windbg檢查匯編代碼:

0:004> !u 000007fe93b83940
Normal JIT generated code
MicroBenchmarks.Program.Boxing()
...
000007fe`93ca01b3 call    System_ni+0x2905e0 (000007fe`f07a05e0) (System.Diagnostics.Stopwatch.GetTimestamp(), mdToken: 00000000060040d2)
...
//This is the for loop
000007fe`93ca01c2 mov     eax,21h
000007fe`93ca01c7 mov     dword ptr [rsp+20h],eax
000007fe`93ca01cb lea     rdx,[rsp+20h]
000007fe`93ca01d0 lea     rcx,[mscorlib_ni+0x6e92b0 (000007fe`f18b92b0)]
//here is the boxing
000007fe`93ca01d7 call    clr!JIT_BoxFastMP_InlineGetThread (000007fe`f33126d0)   
000007fe`93ca01dc mov     rsi,rax
//loop unrolling. instead of increment i by 1, we are actually incrementing i by 4
000007fe`93ca01df add     edi,4                 
000007fe`93ca01e2 cmp     edi,0F4240h           // 0F4240h = 1000000
000007fe`93ca01e8 jl      000007fe`93ca01c2     // jumps to the line "mov eax,21h"
//end of the for loop
000007fe`93ca01ea mov     rcx,rbx
000007fe`93ca01ed call    System_ni+0x2acb70 (000007fe`f07bcb70) (System.Diagnostics.Stopwatch.Stop(), mdToken: 00000000060040cb)

UnBoxing程序集:

0:004> !u 000007fe93b83930
Normal JIT generated code
MicroBenchmarks.Program.Unboxing()
Begin 000007fe93ca02c0, size 117
000007fe`93ca02c0 push    rbx
...
000007fe`93ca030a call    System_ni+0x2905e0 (000007fe`f07a05e0) (System.Diagnostics.Stopwatch.GetTimestamp(), mdToken: 00000000060040d2)
000007fe`93ca030f mov     qword ptr [rbx+10h],rax
000007fe`93ca0313 mov     byte ptr [rbx+18h],1
000007fe`93ca0317 xor     eax,eax
000007fe`93ca0319 mov     edi,dword ptr [rdi+8]
000007fe`93ca031c nop     dword ptr [rax]
//This is the for loop
//again, loop unrolling
000007fe`93ca0320 add     eax,4
000007fe`93ca0323 cmp     eax,0F4240h    // 0F4240h = 1000000
000007fe`93ca0328 jl      000007fe`93ca0320  //jumps to "add eax,4"
//end of the for loop
000007fe`93ca032a mov     rcx,rbx
000007fe`93ca032d call    System_ni+0x2acb70 (000007fe`f07bcb70) (System.Diagnostics.Stopwatch.Stop(), mdToken: 00000000060040cb)

您可以看到即使在IL級別上比較似乎是合理的,JIT仍然可以在運行時執行另一個優化。 UnBoxing方法再次進行空循環。 直到你驗證為兩種方法執行的代碼是可比較的,很難簡單地總結“拆箱比拳擊快10倍”

因為裝箱涉及對象,而拆箱涉及基元。 OOP語言中原語的全部目的是提高性能; 所以它成功了就不足為奇了。

Boxing在堆上創建一個新對象。 像數組初始化一樣:

int[] arr = {10, 20, 30};

boxing提供了方便的初始化語法,因此您不必顯式使用new運算符。 但實際上實例回事。

拆箱要便宜得多:遵循對盒裝值的引用,並檢索值。

拳擊具有在堆上創建引用類型對象的所有開銷。

拆箱只有間接開銷。

考慮一下:對於拳擊你必須分配內存。 對於拆箱,你一定不能。 鑒於拆箱是一項微不足道的操作(特別是在你的情況下,甚至沒有任何事情發生在結果上)。

拳擊和拆箱是計算上昂貴的過程。 裝箱值類型時,必須創建一個全新的對象。 這可能比簡單的參考分配長20倍。 拆箱時,鑄造過程可能需要四倍的分配。

Why unboxing is 100 time faster than boxing

當您鍵入值類型時,必須創建一個新對象,並且必須將值復制到新對象中。 取消裝箱時,只需從裝箱實例中復制該值。 所以拳擊添加了一個對象的創建。 然而,這在.NET中確實很快,因此差異可能不是很大。 如果您需要最大速度,請盡量避免整個拳擊程序。 請記住,裝箱會創建需要由垃圾收集器清理的對象

使程序變慢的一個原因是當你必須移入和移出內存時。 如果沒有必要(如果你想要速度),應該避免訪問內存。

如果我查看拆箱和裝箱你看到的區別在於裝箱在堆上分配內存並且拆箱將值類型變量移動到堆棧。 訪問堆棧比堆快,因此在您的情況下拆箱更快。

堆棧更快,因為訪問模式使得從中分配和釋放內存變得微不足道(指針/整數簡單地遞增或遞減),而堆在分配或免費中涉及更復雜的簿記。 此外,堆棧中的每個字節都經常被頻繁地重用,這意味着它往往被映射到處理器的緩存,使其非常快。 堆的另一個性能損失是堆(主要是全局資源)通常必須是多線程安全的,即每個分配和釋放需要 - 通常 - 與程序中的“所有”其他堆訪問同步。

我從SwankyLegg這里得到了這些信息: 堆棧和堆的內容和位置是什么?

要查看拆箱和裝箱對內存(堆棧和堆)的區別,您可以在此查找: http//msdn.microsoft.com/en-us/library/yz2be5wk.aspx

為了簡單起見,盡可能使用原始類型,如果可以的話,不要引用內存。 如果你真的想要速度,你應該考慮緩存,預取,阻止..

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM